Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report terraform outputs in workspace status #70

Merged
merged 2 commits into from
Jan 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions api/etok.dev/v1alpha1/workspace_types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package v1alpha1

import (
"fmt"

"github.com/leg100/etok/pkg/util/slice"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -100,6 +102,9 @@ type WorkspaceStatus struct {

// True if resource has been reconciled at least once.
Reconciled bool `json:"reconciled,omitempty"`

// Outputs from state file
Outputs []*Output `json:"outputs,omitempty"`
}

// Variable denotes an input to the module
Expand All @@ -115,6 +120,14 @@ type Variable struct {
EnvironmentVariable bool `json:"environmentVariable,omitempty"`
}

// Output outputs the values of Terraform output
type Output struct {
// Attribute name in module
Key string `json:"key"`
// Value
Value string `json:"value"`
}

func (ws *Workspace) IsReconciled() bool {
return ws.Status.Reconciled
}
Expand All @@ -127,6 +140,12 @@ func (ws *Workspace) PVCName() string {
return ws.Name
}

// StateSecretName retrieves the name of the secret containing the terraform
// state for this workspace.
func (ws *Workspace) StateSecretName() string {
return fmt.Sprintf("tfstate-default-%s", ws.Name)
}

func (ws *Workspace) VariablesConfigMapName() string {
return ws.Name + "-variables"
}
Expand Down
16 changes: 16 additions & 0 deletions config/crd/bases/etok.dev_all.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,22 @@ spec:
status:
description: WorkspaceStatus defines the observed state of Workspace
properties:
outputs:
description: Outputs from state file
items:
description: Output outputs the values of Terraform output
properties:
key:
description: Attribute name in module
type: string
value:
description: Value
type: string
required:
- key
- value
type: object
type: array
phase:
description: Lifecycle phase of workspace.
type: string
Expand Down
16 changes: 16 additions & 0 deletions config/crd/bases/etok.dev_workspaces.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,22 @@ spec:
status:
description: WorkspaceStatus defines the observed state of Workspace
properties:
outputs:
description: Outputs from state file
items:
description: Output outputs the values of Terraform output
properties:
key:
description: Attribute name in module
type: string
value:
description: Value
type: string
required:
- key
- value
type: object
type: array
phase:
description: Lifecycle phase of workspace.
type: string
Expand Down
18 changes: 18 additions & 0 deletions pkg/controllers/testdata/tfstate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apiVersion: v1
data:
tfstate: H4sIAAAAAAAA/3SS0a6iMBCG732Kptfi8SBH0cSLfYS92WTjMaTQASYLLZkO5pgN775pDyJq9oKk9Pvnn58Z/i6EkBcgh9bIg0iW/p2BSJWW2uxO5Hr1nqw2MggcEKpm0jdoQFXgVWmSlJu83ERr2OaRLtNd9J7G+2gH+zjfF+t8s9t+e9ieu56dPAifQQhJymjbZo4JTTVd+3iq6YN5+ZEmkYZS9Q1HpbX+CWZBxtcuqEaDcD0shBhCOwJneyrANzwFNvm3VofCVhlVgX51HJPhDBnVBsTg+H7bkb2gBvLkdj59SoIKHdN1NQ12hfatVq7GwlL39u3/Kc93JzSOlZnHnUcOElfU0KrZitbLOVbMhHnP4GazHFm+TTLH2sfc//55lMtX3FMz4hd6ZcgaMBXX8iDiJ6qh8HXbOP2Inytr+Lqt8Rmh/k+3PwAdkP8G0zfNE+wISvwa2QwND5NwYBwyXiB7mMnp/KDqCC+Kw1Jz88vlP45HOfFhPJ2nv+q8GBb/AgAA//+GknaoPQMAAA==
kind: Secret
metadata:
annotations:
encoding: gzip
labels:
app.kubernetes.io/managed-by: terraform
tfstate: "true"
tfstateSecretSuffix: foo
tfstateWorkspace: default
manager: HashiCorp
operation: Update
time: "2020-12-31T18:32:26Z"
name: tfstate-default-foo
namespace: default
type: Opaque
45 changes: 45 additions & 0 deletions pkg/controllers/workspace_controller.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package controllers

import (
"bytes"
"compress/gzip"
"context"
"encoding/json"
"reflect"
"regexp"
"strings"
Expand Down Expand Up @@ -105,6 +108,48 @@ func (r *WorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
return r.success(ctx, &ws)
}

// Retrieve state file
var state corev1.Secret
if err := r.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: ws.StateSecretName()}, &state); err != nil {
// Ignore not found errors and keep on reconciling - the state file
// might not yet have been created
if !errors.IsNotFound(err) {
log.Error(err, "unable to get state secret")
return ctrl.Result{}, err
}
} else {
// State file exists
if val, ok := state.Data["tfstate"]; ok {
gr, err := gzip.NewReader(bytes.NewBuffer(val))
if err != nil {
log.Error(err, "unable to decompress state file")
return ctrl.Result{}, err
}

// Persist outputs from state file to workspace status
var sfile struct {
Outputs map[string]struct {
Type string
Value string
}
}
if err := json.NewDecoder(gr).Decode(&sfile); err != nil {
log.Error(err, "unable to decode state file")
return ctrl.Result{}, err
}
var outputs []*v1alpha1.Output
for k, v := range sfile.Outputs {
outputs = append(outputs, &v1alpha1.Output{Key: k, Value: v.Value})
}
if !reflect.DeepEqual(ws.Status.Outputs, outputs) {
ws.Status.Outputs = outputs
if err := r.Status().Update(ctx, &ws); err != nil {
return ctrl.Result{}, err
}
}
}
}

// Manage ConfigMap containing variables for workspace
var variables corev1.ConfigMap
if err := r.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: ws.VariablesConfigMapName()}, &variables); err != nil {
Expand Down
35 changes: 35 additions & 0 deletions pkg/controllers/workspace_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package controllers

import (
"context"
"os"
"testing"

v1alpha1 "github.com/leg100/etok/api/etok.dev/v1alpha1"
Expand All @@ -12,6 +13,7 @@ import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
Expand Down Expand Up @@ -329,3 +331,36 @@ func TestReconcileWorkspaceVariables(t *testing.T) {
})
}
}

func TestReconcileWorkspaceState(t *testing.T) {
var state corev1.Secret
var workspace = testobj.Workspace("default", "foo")

f, err := os.Open("testdata/tfstate.yaml")
require.NoError(t, yaml.NewYAMLOrJSONDecoder(f, 999).Decode(&state))

cl := fake.NewFakeClientWithScheme(scheme.Scheme, workspace, &state)

r := NewWorkspaceReconciler(cl, "a.b.c.d:v1")

req := reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.Name,
Namespace: workspace.Namespace,
},
}
_, err = r.Reconcile(context.Background(), req)
require.NoError(t, err)

// Fetch fresh workspace for assertions
ws := &v1alpha1.Workspace{}
require.NoError(t, r.Get(context.TODO(), req.NamespacedName, ws))

assert.Equal(t, []*v1alpha1.Output{
{
Key: "random_string",
Value: "f584-default-foo-foo",
},
}, ws.Status.Outputs)

}
3 changes: 0 additions & 3 deletions pkg/testobj/k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,6 @@ func Secret(namespace, name string, opts ...func(*corev1.Secret)) *corev1.Secret
Name: name,
Namespace: namespace,
},
StringData: map[string]string{
"google_application_credentials.json": "abc",
},
}
for _, o := range opts {
o(secret)
Expand Down
8 changes: 8 additions & 0 deletions test/e2e/terraform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ func TestTerraform(t *testing.T) {
}))
})

// Check that both `etok output` works and that the workspace resource
// has the correct output in its status
t.Run("output", func(t *testing.T) {
require.NoError(t, step(t, name,
[]string{buildPath, "output",
Expand All @@ -88,6 +90,12 @@ func TestTerraform(t *testing.T) {
[]expect.Batcher{
&expect.BExp{R: `random_string = "[0-9a-f]{4}-bar-e2e-terraform-foo"`},
}))

ws, err := client.WorkspacesClient(namespace).Get(context.Background(), "foo", metav1.GetOptions{})
require.NoError(t, err)

require.Equal(t, "random_string", ws.Status.Outputs[0].Key)
require.Regexp(t, `[0-9a-f]{4}-bar-e2e-terraform-foo`, ws.Status.Outputs[0].Value)
})

t.Run("destroy", func(t *testing.T) {
Expand Down