Skip to content

Commit

Permalink
Merge pull request #13 from patoarvizu/project-to-secrets
Browse files Browse the repository at this point in the history
Project to secrets
  • Loading branch information
patoarvizu committed Nov 24, 2020
2 parents d4ffb68 + 8992773 commit 054e78a
Show file tree
Hide file tree
Showing 12 changed files with 217 additions and 50 deletions.
9 changes: 5 additions & 4 deletions README.md
Expand Up @@ -21,13 +21,13 @@ The adoption of Terraform in many organizations predates the adoption of Kuberne

Just like [amphibians](https://en.wikipedia.org/wiki/Amphibian) can inhabit both land and water, this project aims to close the interface gap between Terraform outputs and Kubernetes configuration discovery. The existing [terraform-helm](https://github.com/hashicorp/terraform-helm) and [aws-controllers-k8s](https://github.com/aws/aws-controllers-k8s) projects don't yet have the full functionality and flexibility that Amphibian provides.

A [Custom resource](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) of kind `TerraformState` deployed on Kubernetes clusters will create a new `ConfigMap` and populate it with the [outputs](https://www.terraform.io/docs/configuration/outputs.html) of the corresponding remote Terraform state.
A [Custom resource](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) of kind `TerraformState` deployed on Kubernetes clusters will create a new `ConfigMap` or `Secret` and populate it with the [outputs](https://www.terraform.io/docs/configuration/outputs.html) of the corresponding remote Terraform state.

## Design

Even though Terraform has a `struct` for capturing a module's [output values](https://github.com/hashicorp/terraform/blob/v0.13.5/states/output_value.go) programmatically, that API can't be considered public and guaranteed.

Since the only guaranteed interface is the command line, the way this controller gets the outputs from the remote state is by creating a `data.tf` and an `outputs.tf` file, running `terraform apply`, followed by `terraform output -json`, and then unmarshaling that output back into a Go `struct`. The controller then uses those outputs to create a new configmap in the location defined by `target`, that has the exact contents returned by Terraform.
Since the only guaranteed interface is the command line, the way this controller gets the outputs from the remote state is by creating a `data.tf` and an `outputs.tf` file, running `terraform apply`, followed by `terraform output -json`, and then unmarshaling that output back into a Go `struct`. The controller then uses those outputs to create a new `ConfigMap` or `Secret` in the location defined by `target`, that has the exact contents returned by Terraform.

**Note:** Keep in mind that Terraform only returns the [root-level outputs](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/data-sources/remote_state#root-outputs-only). If you need to consume the outputs of a submodule, you'll have to expose it all the way to the root level so they can be discovered in Kubernetes.

Expand Down Expand Up @@ -95,9 +95,10 @@ Additionally, the following options are not available since they're irrelevant f

### Target

The `target` field represents the location where the outputs from the upstream state will be projected. Currently, only projecting onto a `ConfigMap` is supported.
The `target` field represents the location and type of object where the outputs from the upstream state will be projected.

- `configMapName`: The name of the `ConfigMap` that will hold the `outputs` map.
- `type`: The type of object where the outputs will be projected. It supports either `configmap` or `secret` (all lowercase in both cases).
- `name`: The name of either the `ConfigMap` or the `Secret` that will hold the `outputs` map.

#### Values

Expand Down
4 changes: 3 additions & 1 deletion api/v1/terraformstate_types.go
Expand Up @@ -33,7 +33,9 @@ type RemoteConfig struct {
}

type Target struct {
ConfigMapName string `json:"configMapName"`
// +kubebuilder:validation:Enum={"configmap","secret"}
Type string `json:"type"`
Name string `json:"name"`
}

type S3Config struct {
Expand Down
10 changes: 8 additions & 2 deletions config/crd/bases/terraform.patoarvizu.dev_terraformstates.yaml
Expand Up @@ -149,10 +149,16 @@ spec:
type: object
target:
properties:
configMapName:
name:
type: string
type:
enum:
- configmap
- secret
type: string
required:
- configMapName
- name
- type
type: object
type:
type: string
Expand Down
11 changes: 11 additions & 0 deletions config/rbac/role.yaml
Expand Up @@ -17,6 +17,17 @@ rules:
- patch
- update
- watch
- apiGroups:
- ""
resources:
- secrets
verbs:
- create
- get
- list
- patch
- update
- watch
- apiGroups:
- terraform.patoarvizu.dev
resources:
Expand Down
86 changes: 69 additions & 17 deletions controllers/terraformstate_controller.go
Expand Up @@ -55,6 +55,7 @@ type terraformOutputs struct {
// +kubebuilder:rbac:groups=terraform.patoarvizu.dev,resources=terraformstates,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=terraform.patoarvizu.dev,resources=terraformstates/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=core,resources=configmaps,verbs=watch;list;create;get;update;patch
// +kubebuilder:rbac:groups=core,resources=secrets,verbs=watch;list;create;get;update;patch

func (r *TerraformStateReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
ctx := context.Background()
Expand Down Expand Up @@ -157,15 +158,15 @@ func (r *TerraformStateReconciler) Reconcile(req ctrl.Request) (ctrl.Result, err
return ctrl.Result{}, err
}

configMapData := make(map[string]string)
targetData := make(map[string]string)
for k, v := range tfOutputs.Outputs.Value {
s, ok := v.(string)
if ok {
configMapData[k] = s
targetData[k] = s
} else {
data, err := json.Marshal(v)
if err == nil {
configMapData[k] = fmt.Sprintf("%s", data)
targetData[k] = fmt.Sprintf("%s", data)
} else {
r.Log.Info(fmt.Sprintf("Skipping field %s: %v", k, err))
}
Expand All @@ -176,38 +177,89 @@ func (r *TerraformStateReconciler) Reconcile(req ctrl.Request) (ctrl.Result, err
if ok {
resyncPeriod, err = strconv.Atoi(resyncPeriodEnvVar)
}
switch state.Spec.Target.Type {
case "configmap":
err = r.createConfigMap(state, targetData)
case "secret":
err = r.createSecret(state, convertToMapStringByte(targetData))
}
if err != nil {
return ctrl.Result{}, err
}

return ctrl.Result{RequeueAfter: time.Second * time.Duration(resyncPeriod)}, nil
}

func (r *TerraformStateReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&terraformv1.TerraformState{}).
Complete(r)
}

func (r *TerraformStateReconciler) createConfigMap(state *terraformv1.TerraformState, targetData map[string]string) error {
ctx := context.Background()
configMap := &corev1.ConfigMap{}
err = r.Get(ctx, types.NamespacedName{Namespace: state.ObjectMeta.Namespace, Name: state.Spec.Target.ConfigMapName}, configMap)
err := r.Get(ctx, types.NamespacedName{Namespace: state.ObjectMeta.Namespace, Name: state.Spec.Target.Name}, configMap)
if err != nil {
if errors.IsNotFound(err) {
configMap.ObjectMeta.Namespace = state.ObjectMeta.Namespace
configMap.ObjectMeta.Name = state.Spec.Target.ConfigMapName
configMap.Data = configMapData
configMap.ObjectMeta.Name = state.Spec.Target.Name
configMap.Data = targetData
err = ctrl.SetControllerReference(state, configMap, r.Scheme)
if err != nil {
return ctrl.Result{}, err
return err
}
err = r.Create(ctx, configMap)
if err != nil {
return ctrl.Result{}, err
return err
}
return ctrl.Result{RequeueAfter: time.Second * time.Duration(resyncPeriod)}, nil
return nil
}
return ctrl.Result{}, err
return err
}
configMap.Data = configMapData
configMap.Data = targetData
err = r.Update(ctx, configMap)
if err != nil {
return ctrl.Result{}, err
return err
}
return nil
}

return ctrl.Result{RequeueAfter: time.Second * time.Duration(resyncPeriod)}, nil
func (r *TerraformStateReconciler) createSecret(state *terraformv1.TerraformState, targetData map[string][]byte) error {
ctx := context.Background()
secret := &corev1.Secret{}
err := r.Get(ctx, types.NamespacedName{Namespace: state.ObjectMeta.Namespace, Name: state.Spec.Target.Name}, secret)
if err != nil {
if errors.IsNotFound(err) {
secret.ObjectMeta.Namespace = state.ObjectMeta.Namespace
secret.ObjectMeta.Name = state.Spec.Target.Name
secret.Data = targetData
err = ctrl.SetControllerReference(state, secret, r.Scheme)
if err != nil {
return err
}
err = r.Create(ctx, secret)
if err != nil {
return err
}
return nil
}
return err
}
secret.Data = targetData
err = r.Update(ctx, secret)
if err != nil {
return err
}
return nil
}

func (r *TerraformStateReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&terraformv1.TerraformState{}).
Complete(r)
func convertToMapStringByte(data map[string]string) map[string][]byte {
targetData := make(map[string][]byte)
for k, v := range data {
targetData[k] = []byte(v)
}
return targetData
}

func createRemoteBackendBody(config terraformv1.RemoteConfig) cty.Value {
Expand Down
Binary file modified docs/amphibian-0.0.0.tgz
Binary file not shown.
1 change: 1 addition & 0 deletions docs/index.md
Expand Up @@ -12,5 +12,6 @@ Amphibian
| imagePullPolicy | string | `"IfNotPresent"` | The imagePullPolicy to be used on the operator. |
| imageVersion | string | `"latest"` | The image version used for the operator. |
| prometheusMonitoring.enable | bool | `false` | Create the `Service` and `ServiceMonitor` objects to enable Prometheus monitoring on the operator. |
| rbac.clusterRoleSecretsAccessRules | list | `[{"apiGroups":[""],"resources":["secrets"],"verbs":["create","get","list","patch","update","watch"]}]` | List of `PolicyRule`s for accessing Kubernetes secrets, to be appended to the `amphibian-manager-role` cluster role. |
| resources | object | `nil` | The resources requests/limits to be set on the deployment pod spec template. |
| watchNamespace | string | `""` | The value to be set on the `WATCH_NAMESPACE` environment variable. |
6 changes: 3 additions & 3 deletions docs/index.yaml
Expand Up @@ -2,11 +2,11 @@ apiVersion: v1
entries:
amphibian:
- apiVersion: v2
created: "2020-11-21T20:54:45.582011-05:00"
created: "2020-11-23T20:49:40.965603-05:00"
description: Amphibian
digest: 71f3fc2033ab38fc9bd31fd608f4ad8ce032d47af2c709acadc2b41bbc5a7e02
digest: 3c86045380e383b6585b5d14a7cef94452a369975cff5a33073500158a052889
name: amphibian
urls:
- https://patoarvizu.github.io/amphibian/amphibian-0.0.0.tgz
version: 0.0.0
generated: "2020-11-21T20:54:45.581461-05:00"
generated: "2020-11-23T20:49:40.965042-05:00"
10 changes: 8 additions & 2 deletions helm/amphibian/templates/crds/terraformstate.yaml
Expand Up @@ -149,10 +149,16 @@ spec:
type: object
target:
properties:
configMapName:
name:
type: string
type:
enum:
- configmap
- secret
type: string
required:
- configMapName
- name
- type
type: object
type:
type: string
Expand Down
10 changes: 7 additions & 3 deletions helm/amphibian/templates/rbac.yaml
Expand Up @@ -2,7 +2,7 @@ apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
creationTimestamp: null
name: manager-role
name: amphibian-manager-role
rules:
- apiGroups:
- ""
Expand Down Expand Up @@ -35,15 +35,19 @@ rules:
- get
- patch
- update
{{- range .Values.rbac.clusterRoleSecretsAccessRules }}
- {{ range $k, $v := . }}{{ $k }}: {{ toYaml $v | nindent 2 }}
{{ end }}
{{- end }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: manager-rolebinding
name: amphibian-manager-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: manager-role
name: amphibian-manager-role
subjects:
- kind: ServiceAccount
name: amphibian
Expand Down
16 changes: 15 additions & 1 deletion helm/amphibian/values.yaml
Expand Up @@ -11,4 +11,18 @@ prometheusMonitoring:
# authEnvVars -- Environment variables required for remote state backend authentication. This is a slice of [`v1.EnvVar`](https://pkg.go.dev/k8s.io/api/core/v1#EnvVar)s.
authEnvVars:
# resources -- (object) The resources requests/limits to be set on the deployment pod spec template.
resources:
resources:
rbac:
# rbac.clusterRoleSecretsAccessRules -- List of `PolicyRule`s for accessing Kubernetes secrets, to be appended to the `amphibian-manager-role` cluster role.
clusterRoleSecretsAccessRules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- create
- get
- list
- patch
- update
- watch

0 comments on commit 054e78a

Please sign in to comment.