Skip to content

Commit

Permalink
Change installed az CLI version with clientVersion
Browse files Browse the repository at this point in the history
I've added clientVersion to the az mixin configuration so that you can specify which version of the az CLI to install when porter builds the bundle. By default, the latest version of the CLI is installed.

    ```yaml
    mixins:
    - az:
        clientVersion: 2.35.0
    ```

The mixin will use the clientVersion to build the apt package version published by Microsoft, which follows the format VERSION-1~DISTROCODENAME. So in the example above, the package version is 2.35.0-1~stretch. If the user changes the debian version, then stretch will change to match the version, e.g. buster, etc.

Signed-off-by: Carolyn Van Slyck <[email protected]>
  • Loading branch information
carolynvs committed Sep 15, 2022
1 parent e4ff4d3 commit 1d51280
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 47 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ This is a mixin for Porter that provides the Azure (az) CLI.

## Mixin Configuration

### Client Version
By default, the most recent version of the az CLI is installed.
You can specify a specific version with the `clientVersion` setting.

```yaml
mixins:
- az:
clientVersion: 1.2.3
```
### Extensions
When you declare the mixin, you can also configure additional extensions to install
**Use the vanilla az CLI**
Expand Down
32 changes: 25 additions & 7 deletions pkg/az/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package az

import (
"fmt"
"text/template"

"get.porter.sh/porter/pkg/exec/builder"
"gopkg.in/yaml.v3"
Expand All @@ -18,9 +19,26 @@ type BuildInput struct {
// extensions:
// - NAME
type MixinConfig struct {
Extensions []string
ClientVersion string `yaml:"clientVersion,omitempty"`
Extensions []string `yaml:"extensions,omitempty"`
}

// The package version of the az cli follows this format:
// VERSION-1~DISTRO_CODENAME So if we are running on debian stretch and have a
// version of 1.2.3, the package version would be 1.2.3-1~stretch.
const buildTemplate string = `
RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/apt \
apt-get update && apt-get install -y apt-transport-https lsb-release gnupg curl
RUN curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /etc/apt/trusted.gpg.d/microsoft.asc.gpg
RUN echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/azure-cli.list
RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/apt \
apt-get update && apt-get install -y --no-install-recommends \
{{ if eq .ClientVersion ""}}azure-cli{{else}}azure-cli={{.ClientVersion}}-1~$(lsb_release -cs){{end}}
{{ range $ext := .Extensions }}
RUN az extension add -y --name {{ $ext }}
{{ end }}
`

// Build installs the az cli and any configured extensions.
func (m *Mixin) Build() error {
var input BuildInput
Expand All @@ -32,13 +50,13 @@ func (m *Mixin) Build() error {
return err
}

fmt.Fprintln(m.Out, `RUN apt-get update && apt-get install -y apt-transport-https lsb-release gnupg curl`)
fmt.Fprintln(m.Out, `RUN curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /etc/apt/trusted.gpg.d/microsoft.asc.gpg`)
fmt.Fprintln(m.Out, `RUN echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/azure-cli.list`)
fmt.Fprintln(m.Out, `RUN apt-get update && apt-get install -y azure-cli`)
tmpl, err := template.New("dockerfile").Parse(buildTemplate)
if err != nil {
return fmt.Errorf("error parsing Dockerfile template for the az mixin: %w", err)
}

for _, ext := range input.Config.Extensions {
fmt.Fprintf(m.Out, "RUN az extension add -y --name %s\n", ext)
if err = tmpl.Execute(m.Out, input.Config); err != nil {
return fmt.Errorf("error generating Dockerfile lines for the az mixin: %w", err)
}

return nil
Expand Down
70 changes: 30 additions & 40 deletions pkg/az/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,38 @@ package az

import (
"bytes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"io/ioutil"
"testing"

"get.porter.sh/porter/pkg/test"

"github.com/stretchr/testify/require"
)

func TestMixin_Build(t *testing.T) {
const buildOutput = `RUN apt-get update && apt-get install -y apt-transport-https lsb-release gnupg curl
RUN curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /etc/apt/trusted.gpg.d/microsoft.asc.gpg
RUN echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/azure-cli.list
RUN apt-get update && apt-get install -y azure-cli
`

t.Run("build with config", func(t *testing.T) {
b, err := ioutil.ReadFile("testdata/build-input-with-config.yaml")
require.NoError(t, err)

m := NewTestMixin(t)
m.Debug = false
m.In = bytes.NewReader(b)

err = m.Build()
require.NoError(t, err, "build failed")

wantOutput := buildOutput + `RUN az extension add -y --name iot
`
gotOutput := m.TestContext.GetOutput()
assert.Equal(t, wantOutput, gotOutput)
})

t.Run("build without config", func(t *testing.T) {
b, err := ioutil.ReadFile("testdata/build-input-without-config.yaml")
require.NoError(t, err)

m := NewTestMixin(t)
m.Debug = false
m.In = bytes.NewReader(b)

err = m.Build()
require.NoError(t, err, "build failed")

gotOutput := m.TestContext.GetOutput()
assert.Equal(t, buildOutput, gotOutput)
})
}

testcases := []struct {
name string
inputFile string
wantOutputFile string
}{
{name: "build with config", inputFile: "testdata/build-input-with-config.yaml", wantOutputFile: "testdata/build-with-config.txt"},
{name: "build without config", inputFile: "testdata/build-input-without-config.yaml", wantOutputFile: "testdata/build-without-config.txt"},
}

for _, tc := range testcases {
t.Run("build with config", func(t *testing.T) {
b, err := ioutil.ReadFile(tc.inputFile)
require.NoError(t, err)

m := NewTestMixin(t)
m.Debug = false
m.In = bytes.NewReader(b)

err = m.Build()
require.NoError(t, err, "build failed")

test.CompareGoldenFile(t, tc.wantOutputFile, m.TestContext.GetOutput())
})
}
}
33 changes: 33 additions & 0 deletions pkg/az/schema/schema.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,35 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"declaration": {
"oneOf": [
{
"description": "Declare the az mixin without configuration",
"type": "string",
"enum": ["az"]
},
{"$ref": "#/definitions/config"}
]
},
"config": {
"description": "Declare the az mixin with additional configuration",
"type": "object",
"properties": {
"az": {
"description": "az mixin configuration",
"type": "object",
"properties": {
"clientVersion": {
"description": "Version of az to install in the bundle",
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false,
"required": ["az"]
},
"installStep": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -173,6 +202,10 @@
"items": {
"$ref": "#/definitions/uninstallStep"
}
},
"mixins": {
"type": "array",
"items": { "$ref": "#/definitions/declaration" }
}
},
"additionalProperties": {
Expand Down
36 changes: 36 additions & 0 deletions pkg/az/schema_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package az

import (
"encoding/json"
"fmt"
"io/ioutil"
"testing"

"github.com/PaesslerAG/jsonpath"
"github.com/ghodss/yaml" // We are not using go-yaml because of serialization problems with jsonschema, don't use this library elsewhere
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -65,3 +67,37 @@ func TestMixin_ValidateSchema(t *testing.T) {
})
}
}

func TestMixin_CheckSchema(t *testing.T) {
// Long term it would be great to have a helper function in Porter that a mixin can use to check that it meets certain interfaces
// check that certain characteristics of the schema that Porter expects are present
// Once we have a mixin library, that would be a good place to package up this type of helper function
var schemaMap map[string]interface{}
err := json.Unmarshal([]byte(schema), &schemaMap)
require.NoError(t, err, "could not unmarshal the schema into a map")

t.Run("mixin configuration", func(t *testing.T) {
// Check that mixin config is defined, and has all the supported fields
configSchema, err := jsonpath.Get("$.definitions.config", schemaMap)
require.NoError(t, err, "could not find the mixin config schema declaration")
_, err = jsonpath.Get("$.properties.az.properties.clientVersion", configSchema)
require.NoError(t, err, "clientVersion was not included in the mixin config schema")
})

// Check that schema are defined for each action
actions := []string{"install", "upgrade", "invoke", "uninstall"}
for _, action := range actions {
t.Run("supports "+action, func(t *testing.T) {
actionPath := fmt.Sprintf("$.definitions.%sStep", action)
_, err := jsonpath.Get(actionPath, schemaMap)
require.NoErrorf(t, err, "could not find the %sStep declaration", action)
})
}

// Check that the invoke action is registered
additionalSchema, err := jsonpath.Get("$.additionalProperties.items", schemaMap)
require.NoError(t, err, "the invoke action was not registered in the schema")
require.Contains(t, additionalSchema, "$ref")
invokeRef := additionalSchema.(map[string]interface{})["$ref"]
require.Equal(t, "#/definitions/invokeStep", invokeRef, "the invoke action was not registered correctly")
}
2 changes: 2 additions & 0 deletions pkg/az/testdata/build-input-with-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
config:
clientVersion: 1.2.3
extensions:
- iot

actions:
install:
- az:
Expand Down
11 changes: 11 additions & 0 deletions pkg/az/testdata/build-with-config.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/apt \
apt-get update && apt-get install -y apt-transport-https lsb-release gnupg curl
RUN curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /etc/apt/trusted.gpg.d/microsoft.asc.gpg
RUN echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/azure-cli.list
RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/apt \
apt-get update && apt-get install -y --no-install-recommends \
azure-cli=1.2.3-1~$(lsb_release -cs)

RUN az extension add -y --name iot

9 changes: 9 additions & 0 deletions pkg/az/testdata/build-without-config.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/apt \
apt-get update && apt-get install -y apt-transport-https lsb-release gnupg curl
RUN curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /etc/apt/trusted.gpg.d/microsoft.asc.gpg
RUN echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/azure-cli.list
RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/apt \
apt-get update && apt-get install -y --no-install-recommends \
azure-cli

0 comments on commit 1d51280

Please sign in to comment.