diff --git a/.gitignore b/.gitignore index 6b6d323..7594d86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ bin .cnab *-packr.go +.vscode/* \ No newline at end of file diff --git a/README.md b/README.md index 5e660fd..02132cc 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Helm client ```yaml - helm3: - clientVersion: v3.2.1 + clientVersion: v3.3.0 ``` Repositories @@ -96,6 +96,17 @@ outputs: key: SECRET_KEY ``` +The mixin also supports extracting resource metadata from Kubernetes as outputs. + +```yaml +outputs: + - name: NAME + resourceType: RESOURCE_TYPE + resourceName: RESOURCE_TYPE_NAME + namespace: NAMESPACE + jsonPath: JSON_PATH_DEFINITION +``` + ### Examples Install @@ -119,6 +130,11 @@ install: - name: mysql-password secret: mydb-mysql key: mysql-password + - name: mysql-cluster-ip + resourceType: service + resourceName: porter-ci-mysql-service + namespace: "default" + jsonPath: "{.spec.clusterIP}" ``` Upgrade diff --git a/cmd/helm3/main.go b/cmd/helm3/main.go index 0f868f4..21559bb 100644 --- a/cmd/helm3/main.go +++ b/cmd/helm3/main.go @@ -27,7 +27,7 @@ func buildRootCommand(in io.Reader) (*cobra.Command, error) { m.In = in cmd := &cobra.Command{ Use: "helm3", - Long: "A skeleton mixin to use for building other mixins for porter 👩🏽‍✈️", + Long: "A helm3 mixin to use to deploy your resources with porter 👩🏽‍✈️", PersistentPreRun: func(cmd *cobra.Command, args []string) { // Enable swapping out stdout/stderr for testing m.Out = cmd.OutOrStdout() diff --git a/example/porter.yaml b/example/porter.yaml index 646d8d3..a632784 100644 --- a/example/porter.yaml +++ b/example/porter.yaml @@ -1,6 +1,6 @@ mixins: - helm3: - clientVersion: v3.2.1 + clientVersion: v3.3.0 repositories: stable: url: "https://kubernetes-charts.storage.googleapis.com" diff --git a/pkg/helm3/build.go b/pkg/helm3/build.go index 964c6b6..fc0f25d 100644 --- a/pkg/helm3/build.go +++ b/pkg/helm3/build.go @@ -11,7 +11,7 @@ import ( ) // clientVersionConstraint represents the semver constraint for the Helm client version -// Currently, this mixin only supports Helm clients versioned v2.x.x +// Currently, this mixin only supports Helm clients versioned v3.x.x const clientVersionConstraint string = "^v3.x" // BuildInput represents stdin passed to the mixin for the build command. @@ -19,9 +19,10 @@ type BuildInput struct { Config MixinConfig } -// MixinConfig represents configuration that can be set on the helm mixin in porter.yaml +// MixinConfig represents configuration that can be set on the helm3 mixin in porter.yaml // mixins: // - helm3: +// clientVersion: v3.3.0 // repositories: // stable: // url: "https://kubernetes-charts.storage.googleapis.com" diff --git a/pkg/helm3/build_test.go b/pkg/helm3/build_test.go index 01972d9..27f7217 100644 --- a/pkg/helm3/build_test.go +++ b/pkg/helm3/build_test.go @@ -89,6 +89,6 @@ RUN mv linux-amd64/helm /usr/local/bin/helm3` m.Debug = false m.In = bytes.NewReader(b) err = m.Build() - require.EqualError(t, err, `supplied client version "v3.2.1.0" cannot be parsed as semver: Invalid Semantic Version`) + require.EqualError(t, err, `supplied client version "v3.3.0.0" cannot be parsed as semver: Invalid Semantic Version`) }) } diff --git a/pkg/helm3/execute.go b/pkg/helm3/execute.go index 2b5d0cf..0bc1027 100644 --- a/pkg/helm3/execute.go +++ b/pkg/helm3/execute.go @@ -35,17 +35,6 @@ func (m *Mixin) Execute() error { return errors.Wrap(err, "couldn't get kubernetes client") } - for _, output := range step.Outputs { - val, err := getSecret(kubeClient, step.Namespace, output.Secret, output.Key) - if err != nil { - return err - } - - err = m.Context.WriteMixinOutputToFile(output.Name, val) - if err != nil { - return errors.Wrapf(err, "unable to write output '%s'", output.Name) - } - } - - return nil + err = m.handleOutputs(kubeClient, step.Namespace, step.Outputs) + return err } diff --git a/pkg/helm3/helm3.go b/pkg/helm3/helm3.go index 588d69c..52f1f0a 100644 --- a/pkg/helm3/helm3.go +++ b/pkg/helm3/helm3.go @@ -16,7 +16,7 @@ import ( k8s "k8s.io/client-go/kubernetes" ) -const defaultHelmClientVersion string = "v3.2.1" +const defaultHelmClientVersion string = "v3.3.0" // Helm is the logic behind the helm mixin type Mixin struct { diff --git a/pkg/helm3/helpers.go b/pkg/helm3/helpers.go index 47bc806..5902237 100644 --- a/pkg/helm3/helpers.go +++ b/pkg/helm3/helpers.go @@ -8,7 +8,7 @@ import ( testclient "k8s.io/client-go/kubernetes/fake" ) -const MockHelmClientVersion string = "v3.2.1" +const MockHelmClientVersion string = "v3.3.0" type TestMixin struct { *Mixin diff --git a/pkg/helm3/install.go b/pkg/helm3/install.go index 357a6a1..aa9c775 100644 --- a/pkg/helm3/install.go +++ b/pkg/helm3/install.go @@ -100,20 +100,8 @@ func (m *Mixin) Install() error { if err != nil { return err } - // Handle outputs that where generate throw out the steps - for _, output := range step.Outputs { - val, err := getSecret(kubeClient, step.Namespace, output.Secret, output.Key) - if err != nil { - return err - } - - err = m.Context.WriteMixinOutputToFile(output.Name, val) - if err != nil { - return errors.Wrapf(err, "unable to write output '%s'", output.Name) - } - } - - return nil + err = m.handleOutputs(kubeClient, step.Namespace, step.Outputs) + return err } // Prepare set arguments diff --git a/pkg/helm3/install_test.go b/pkg/helm3/install_test.go index cb36128..ab4f493 100644 --- a/pkg/helm3/install_test.go +++ b/pkg/helm3/install_test.go @@ -36,7 +36,8 @@ func TestMixin_UnmarshalInstallStep(t *testing.T) { assert.Equal(t, "Install MySQL", step.Description) assert.NotEmpty(t, step.Outputs) - assert.Equal(t, HelmOutput{"mysql-root-password", "porter-ci-mysql", "mysql-root-password"}, step.Outputs[0]) + assert.Equal(t, HelmOutput{"mysql-root-password", "porter-ci-mysql", "mysql-root-password", "", "", "", ""}, step.Outputs[0]) + assert.Equal(t, HelmOutput{"mysql-cluster-ip", "", "", "service", "porter-ci-mysql-service", "default", "{.spec.clusterIP}"}, step.Outputs[2]) assert.Equal(t, "stable/mysql", step.Chart) assert.Equal(t, "0.10.2", step.Version) assert.Equal(t, true, step.Replace) diff --git a/pkg/helm3/outputs.go b/pkg/helm3/outputs.go index f96352d..3576041 100644 --- a/pkg/helm3/outputs.go +++ b/pkg/helm3/outputs.go @@ -2,7 +2,9 @@ package helm3 import ( "fmt" + "strings" + "github.com/pkg/errors" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" @@ -22,3 +24,61 @@ func getSecret(client kubernetes.Interface, namespace, name, key string) ([]byte } return val, nil } + +func (m *Mixin) getOutput(resourceType, resourceName, namespace, jsonPath string) ([]byte, error) { + args := []string{"get", resourceType, resourceName} + args = append(args, fmt.Sprintf("-o=jsonpath='%s'", jsonPath)) + if namespace != "" { + args = append(args, fmt.Sprintf("--namespace=%s", namespace)) + } + cmd := m.NewCommand("kubectl", args...) + cmd.Stderr = m.Err + out, err := cmd.Output() + if err != nil { + prettyCmd := fmt.Sprintf("%s%s", cmd.Dir, strings.Join(cmd.Args, " ")) + return nil, errors.Wrap(err, fmt.Sprintf("couldn't run command %s", prettyCmd)) + } + return out, nil +} + +func (m *Mixin) handleOutputs(client kubernetes.Interface, namespace string, outputs []HelmOutput) error { + var outputError error + //Now get the outputs + for _, output := range outputs { + + if output.Secret != "" && output.Key != "" { + // Override namespace if output.Namespace is set + if output.Namespace != "" { + namespace = output.Namespace + } + + val, err := getSecret(client, namespace, output.Secret, output.Key) + + if err != nil { + return err + } + + outputError = m.Context.WriteMixinOutputToFile(output.Name, val) + } + + if output.ResourceType != "" && output.ResourceName != "" && output.JSONPath != "" { + bytes, err := m.getOutput( + output.ResourceType, + output.ResourceName, + output.Namespace, + output.JSONPath, + ) + if err != nil { + return err + } + + outputError = m.Context.WriteMixinOutputToFile(output.Name, bytes) + + } + + if outputError != nil { + return errors.Wrapf(outputError, "unable to write output '%s'", output.Name) + } + } + return nil +} diff --git a/pkg/helm3/schema/schema.json b/pkg/helm3/schema/schema.json index 3ebd5b1..ef8ed1d 100644 --- a/pkg/helm3/schema/schema.json +++ b/pkg/helm3/schema/schema.json @@ -157,18 +157,21 @@ "key": { "type": "string" }, - "JSONPath": { + "namespace": { "type": "string" }, - "regex": { + "resourceType": { "type": "string" }, - "path": { + "resourceName": { + "type": "string" + }, + "jsonPath": { "type": "string" } }, "additionalProperties": false, - "required": ["name", "secret", "key"] + "required": ["name"] } }, "helm3": { diff --git a/pkg/helm3/step.go b/pkg/helm3/step.go index c9d1273..7a6bbe0 100644 --- a/pkg/helm3/step.go +++ b/pkg/helm3/step.go @@ -6,7 +6,11 @@ type Step struct { } type HelmOutput struct { - Name string `yaml:"name"` - Secret string `yaml:"secret"` - Key string `yaml:"key"` + Name string `yaml:"name"` + Secret string `yaml:"secret,omitempty"` + Key string `yaml:"key,omitempty"` + ResourceType string `yaml:"resourceType,omitempty"` + ResourceName string `yaml:"resourceName,omitempty"` + Namespace string `yaml:"namespace,omitempty"` + JSONPath string `yaml:"jsonPath,omitempty"` } diff --git a/pkg/helm3/testdata/build-input-with-invalid-client-version.yaml b/pkg/helm3/testdata/build-input-with-invalid-client-version.yaml index ef67caf..9ef7de5 100644 --- a/pkg/helm3/testdata/build-input-with-invalid-client-version.yaml +++ b/pkg/helm3/testdata/build-input-with-invalid-client-version.yaml @@ -1,5 +1,5 @@ config: - clientVersion: v3.2.1.0 + clientVersion: v3.3.0.0 install: - helm3: description: "Install MySQL" diff --git a/pkg/helm3/testdata/build-input-with-supported-client-version.yaml b/pkg/helm3/testdata/build-input-with-supported-client-version.yaml index ae7e5a4..ca4a672 100644 --- a/pkg/helm3/testdata/build-input-with-supported-client-version.yaml +++ b/pkg/helm3/testdata/build-input-with-supported-client-version.yaml @@ -1,5 +1,5 @@ config: - clientVersion: v3.2.1 + clientVersion: v3.3.0 install: - helm3: description: "Install MySQL" diff --git a/pkg/helm3/testdata/build-input-with-version.yaml b/pkg/helm3/testdata/build-input-with-version.yaml index c627866..5ace689 100644 --- a/pkg/helm3/testdata/build-input-with-version.yaml +++ b/pkg/helm3/testdata/build-input-with-version.yaml @@ -1,5 +1,5 @@ config: - version: v3.2.1 + version: v3.3.0 install: - helm3: description: "Install MySQL" diff --git a/pkg/helm3/testdata/install-input.yaml b/pkg/helm3/testdata/install-input.yaml index dfc0c2c..d8fdaa0 100644 --- a/pkg/helm3/testdata/install-input.yaml +++ b/pkg/helm3/testdata/install-input.yaml @@ -17,3 +17,8 @@ install: - name: mysql-password secret: porter-ci-mysql key: mysql-password + - name: mysql-cluster-ip + resourceType: service + resourceName: porter-ci-mysql-service + namespace: "default" + jsonPath: "{.spec.clusterIP}" \ No newline at end of file diff --git a/pkg/helm3/testdata/schema.json b/pkg/helm3/testdata/schema.json index 3ebd5b1..ef8ed1d 100644 --- a/pkg/helm3/testdata/schema.json +++ b/pkg/helm3/testdata/schema.json @@ -157,18 +157,21 @@ "key": { "type": "string" }, - "JSONPath": { + "namespace": { "type": "string" }, - "regex": { + "resourceType": { "type": "string" }, - "path": { + "resourceName": { + "type": "string" + }, + "jsonPath": { "type": "string" } }, "additionalProperties": false, - "required": ["name", "secret", "key"] + "required": ["name"] } }, "helm3": { diff --git a/pkg/helm3/testdata/upgrade-input.yaml b/pkg/helm3/testdata/upgrade-input.yaml index c3692d0..4e0044f 100644 --- a/pkg/helm3/testdata/upgrade-input.yaml +++ b/pkg/helm3/testdata/upgrade-input.yaml @@ -19,3 +19,8 @@ upgrade: - name: mysql-password secret: porter-ci-mysql key: mysql-password + - name: mysql-cluster-ip + resourceType: service + resourceName: porter-ci-mysql-service + namespace: "default" + jsonPath: "{.spec.clusterIP}" \ No newline at end of file diff --git a/pkg/helm3/upgrade.go b/pkg/helm3/upgrade.go index b254e8f..738da14 100644 --- a/pkg/helm3/upgrade.go +++ b/pkg/helm3/upgrade.go @@ -107,16 +107,6 @@ func (m *Mixin) Upgrade() error { return err } - for _, output := range step.Outputs { - val, err := getSecret(kubeClient, step.Namespace, output.Secret, output.Key) - if err != nil { - return err - } - - err = m.Context.WriteMixinOutputToFile(output.Name, val) - if err != nil { - return errors.Wrapf(err, "unable to write output '%s'", output.Name) - } - } - return nil + err = m.handleOutputs(kubeClient, step.Namespace, step.Outputs) + return err } diff --git a/pkg/helm3/upgrade_test.go b/pkg/helm3/upgrade_test.go index 9e2143a..abcac80 100644 --- a/pkg/helm3/upgrade_test.go +++ b/pkg/helm3/upgrade_test.go @@ -30,8 +30,8 @@ func TestMixin_UnmarshalUpgradeStep(t *testing.T) { assert.Equal(t, "Upgrade MySQL", step.Description) assert.NotEmpty(t, step.Outputs) - assert.Equal(t, HelmOutput{"mysql-root-password", "porter-ci-mysql", "mysql-root-password"}, step.Outputs[0]) - + assert.Equal(t, HelmOutput{"mysql-root-password", "porter-ci-mysql", "mysql-root-password", "", "", "", ""}, step.Outputs[0]) + assert.Equal(t, HelmOutput{"mysql-cluster-ip", "", "", "service", "porter-ci-mysql-service", "default", "{.spec.clusterIP}"}, step.Outputs[2]) assert.Equal(t, "stable/mysql", step.Chart) assert.Equal(t, "0.10.2", step.Version) assert.True(t, step.Wait) diff --git a/tests/schema_integration_test.go b/tests/schema_integration_test.go index d0321ce..546fc76 100644 --- a/tests/schema_integration_test.go +++ b/tests/schema_integration_test.go @@ -22,8 +22,7 @@ func TestSchema(t *testing.T) { require.NoError(t, err, "failed to sabotage the schema.json") output := &bytes.Buffer{} - cmd := exec.Command("helm", "schema") - cmd.Path = "../bin/mixins/helm3/helm3" + cmd := exec.Command("../bin/mixins/helm3/helm3", "schema") cmd.Stdout = output cmd.Stderr = output