Skip to content

Commit

Permalink
Add the ability to specify a generic Kubernetes resource output
Browse files Browse the repository at this point in the history
  • Loading branch information
MChorfa committed Aug 18, 2020
1 parent 1fd2a0f commit b466bbb
Show file tree
Hide file tree
Showing 23 changed files with 131 additions and 66 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
bin
.cnab
*-packr.go
.vscode/*
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Helm client

```yaml
- helm3:
clientVersion: v3.2.1
clientVersion: v3.3.0
```
Repositories
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion cmd/helm3/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion example/porter.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
mixins:
- helm3:
clientVersion: v3.2.1
clientVersion: v3.3.0
repositories:
stable:
url: "https://kubernetes-charts.storage.googleapis.com"
Expand Down
5 changes: 3 additions & 2 deletions pkg/helm3/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,18 @@ 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.
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"
Expand Down
2 changes: 1 addition & 1 deletion pkg/helm3/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
})
}
15 changes: 2 additions & 13 deletions pkg/helm3/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion pkg/helm3/helm3.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/helm3/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 2 additions & 14 deletions pkg/helm3/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion pkg/helm3/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
60 changes: 60 additions & 0 deletions pkg/helm3/outputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
}
11 changes: 7 additions & 4 deletions pkg/helm3/schema/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
10 changes: 7 additions & 3 deletions pkg/helm3/step.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
config:
clientVersion: v3.2.1.0
clientVersion: v3.3.0.0
install:
- helm3:
description: "Install MySQL"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
config:
clientVersion: v3.2.1
clientVersion: v3.3.0
install:
- helm3:
description: "Install MySQL"
Expand Down
2 changes: 1 addition & 1 deletion pkg/helm3/testdata/build-input-with-version.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
config:
version: v3.2.1
version: v3.3.0
install:
- helm3:
description: "Install MySQL"
Expand Down
5 changes: 5 additions & 0 deletions pkg/helm3/testdata/install-input.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
11 changes: 7 additions & 4 deletions pkg/helm3/testdata/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
5 changes: 5 additions & 0 deletions pkg/helm3/testdata/upgrade-input.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
14 changes: 2 additions & 12 deletions pkg/helm3/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
4 changes: 2 additions & 2 deletions pkg/helm3/upgrade_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 1 addition & 2 deletions tests/schema_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit b466bbb

Please sign in to comment.