diff --git a/docs/file-reference.md b/docs/file-reference.md index 8389383ab..8225c97a0 100644 --- a/docs/file-reference.md +++ b/docs/file-reference.md @@ -12,14 +12,13 @@ name: database containers: - image: mariadb:10 env: - - name: MYSQL_ROOT_PASSWORD - valueFrom: - secretKeyRef: - name: wordpress - key: MYSQL_ROOT_PASSWORD + - name: APPVERSION + value: 0.3.0 envFrom: - configMapRef: name: database + - secretRef: + name: wordpress volumeMounts: - name: database mountPath: /var/lib/mysql @@ -39,6 +38,10 @@ volumeClaims: configMaps: - data: MYSQL_DATABASE: wordpress +secrets: +- name: wordpress + data: + MYSQL_ROOT_PASSWORD: YWRtaW4= ``` # Root level constructs @@ -120,17 +123,16 @@ simultaneously then the tool will error out. envFrom: - configMapRef: name: +- secretRef: + name: ``` This is similar to the envFrom field in container which is added since Kubernetes -1.6. `envFrom` is a list of references. Right now the only reference that is -supported is of `configMap`. The `configMap` that you refer here, all the data -from that `configMap` will be populated as `env` inside the container. +1.6. All the data from the ConfigMaps and Secrets referred here will be populated +as `env` inside the container. -The restriction being that the `configMap` also has to be defined in the file. -If the `configMap` is not defined in the file under the root level field called -`configMaps`, the tool will throw an error, since it has no way of knowing -from where to populate the environment variables from. +The restriction is that the ConfigMaps and Secrets also have to be defined in the +file since there is no way to get the data to be populated. To read more about this field from the Kubernetes upstream docs see this: https://kubernetes.io/docs/api-reference/v1.6/#envfromsource-v1-core @@ -401,6 +403,61 @@ More info about Probe: https://kubernetes.io/docs/api-reference/v1.6/#probe-v1-c The name of the Ingress. +## secrets + +```yaml +secrets: +- +- +``` + +| **Type** | **Required** | +|----------------------------------|--------------| +| array of [secret](#secret) | no | + +###secret + +```yaml +name: string + +``` + +The Kubernetes Secret resource is being reused here. +More info: https://kubernetes.io/docs/api-reference/v1.6/#secret-v1-core + +So, the Kubernetes Secret resource allows specifying the secret data as base64 +encoded as well as in plaintext. +This would look in kedge as: + +```yaml +secrets: +- name: + data: + : + stringData: + : +``` + +example: + +```yaml +secrets: +- name: wordpress + data: + MYSQL_ROOT_PASSWORD: YWRtaW4= + MYSQL_PASSWORD: cGFzc3dvcmQ= +``` + +#### Name + +`name: wordpress` + +| **Type** | **Required** | +|----------|--------------| +| string | yes | + +The name of the secret. This is a mandatory field. + ## Complete example ```yaml @@ -408,14 +465,13 @@ name: database containers: - image: mariadb:10 env: - - name: MYSQL_ROOT_PASSWORD - valueFrom: - secretKeyRef: - name: wordpress - key: MYSQL_ROOT_PASSWORD + - name: VERSION + value: v0.3.0 envFrom: - configMapRef: name: database + - secretRef: + name: wordpress volumeMounts: - name: database mountPath: /var/lib/mysql @@ -455,4 +511,8 @@ volumeClaims: configMaps: - data: MYSQL_DATABASE: wordpress +secrets: +- name: wordpress + data: + MYSQL_ROOT_PASSWORD: YWRtaW4= ``` diff --git a/examples/secrets/README.md b/examples/secrets/README.md index 91a3bcdfd..b4d52cfc2 100644 --- a/examples/secrets/README.md +++ b/examples/secrets/README.md @@ -1,15 +1,26 @@ # Using secrets -Using secret is similar to any other pod. As of now this does not provide a way to create a secret but only to consume it. - Creating secret: -```bash -oc create secret generic wordpress --from-literal='MYSQL_ROOT_PASSWORD=rootpasswd,DB_PASSWD=wordpress' +Create a secret by defining it at the root level - +```yaml +secrets: +- name: wordpress + data: + MYSQL_ROOT_PASSWORD: YWRtaW4= + MYSQL_PASSWORD: cGFzc3dvcmQ= ``` Now consuming it, see the snippet from [db.yaml](db.yaml): +```yaml + envFrom: + - secretRef: + name: wordpress +``` + +Alternatively, it can also be consumed by referencing it manually in `env:` + ```yaml env: - name: MYSQL_ROOT_PASSWORD diff --git a/examples/secrets/db.yaml b/examples/secrets/db.yaml index 6ca440ebb..030f60c74 100644 --- a/examples/secrets/db.yaml +++ b/examples/secrets/db.yaml @@ -2,21 +2,19 @@ name: database containers: - image: mariadb:10 env: - - name: MYSQL_ROOT_PASSWORD - valueFrom: - secretKeyRef: - name: wordpress - key: MYSQL_ROOT_PASSWORD - name: MYSQL_DATABASE value: wordpress - name: MYSQL_USER value: wordpress - - name: MYSQL_PASSWORD - valueFrom: - secretKeyRef: - name: wordpress - key: DB_PASSWD + envFrom: + - secretRef: + name: wordpress services: - name: database ports: - port: 3306 +secrets: +- name: wordpress + data: + MYSQL_ROOT_PASSWORD: YWRtaW4= + MYSQL_PASSWORD: cGFzc3dvcmQ= diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index d225def56..63f63d820 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -45,9 +45,10 @@ type IngressSpecMod struct { ext_v1beta1.IngressSpec `json:",inline"` } -// EnvFromSource represents the source of a set of ConfigMaps +// EnvFromSource represents the source of a set of ConfigMaps and Secrets type EnvFromSource struct { ConfigMapRef *ConfigMapEnvSource `json:"configMapRef,omitempty"` + SecretRef *SecretEnvSource `json:"secretRef,omitempty"` } // ConfigMapEnvSource selects a ConfigMap to populate the environment @@ -56,6 +57,11 @@ type ConfigMapEnvSource struct { Name string `json:"name,omitempty"` } +// SecretEnvSource selects a Secret to populate the environment variables with. +type SecretEnvSource struct { + Name string `json:"name,omitempty"` +} + type Container struct { // one common definitions for livenessProbe and readinessProbe // this allows to have only one place to define both probes (if they are the same) @@ -75,6 +81,11 @@ type PodSpecMod struct { api_v1.PodSpec `json:",inline"` } +type SecretMod struct { + Name string `json:"name,omitempty"` + api_v1.Secret `json:",inline"` +} + type App struct { Name string `json:"name"` Labels map[string]string `json:"labels,omitempty"` @@ -82,6 +93,7 @@ type App struct { ConfigMaps []ConfigMapMod `json:"configMaps,omitempty"` Services []ServiceSpecMod `json:"services,omitempty"` Ingresses []IngressSpecMod `json:"ingresses,omitempty"` + Secrets []SecretMod `json:"secrets,omitempty"` PodSpecMod `json:",inline"` ext_v1beta1.DeploymentSpec `json:",inline"` } diff --git a/pkg/transform/kubernetes/kubernetes.go b/pkg/transform/kubernetes/kubernetes.go index 053b1eb17..ced989d97 100644 --- a/pkg/transform/kubernetes/kubernetes.go +++ b/pkg/transform/kubernetes/kubernetes.go @@ -280,59 +280,113 @@ func populateContainerHealth(app *spec.App) error { return nil } +func getConfigMapData(name string, configMaps []spec.ConfigMapMod) (map[string]string, error) { + for _, cm := range configMaps { + if cm.Name == name { + return cm.Data, nil + } + } + return nil, fmt.Errorf("configMap %v not defined", name) +} + +func getSecretDataKeys(name string, secrets []spec.SecretMod) ([]string, error) { + var dataKeys []string + for _, secret := range secrets { + if secret.Name == name { + for dk := range secret.Data { + dataKeys = append(dataKeys, dk) + } + for sdk := range secret.StringData { + dataKeys = append(dataKeys, sdk) + } + return dataKeys, nil + } + } + return nil, fmt.Errorf("secret %v not defined", name) +} + func populateEnvFrom(app *spec.App) error { // iterate on the containers so that we can extract envFrom // that we have custom defined for ci, c := range app.Containers { - for ei, e := range c.EnvFrom { - cmName := e.ConfigMapRef.Name - - // we also need to check if the configMap specified if it exists - var cmFound bool - // to populate the envs we also need to know the data - // from configmaps defined in the app - for _, cm := range app.ConfigMaps { - // we will only populate the configMap that is specified - // not every configMap out there - if cm.Name != cmName { - continue + for _, e := range c.EnvFrom { + var envs []api_v1.EnvVar + + // populating ConfigMaps + if e.ConfigMapRef != nil { + rootConfigMapData, err := getConfigMapData(e.ConfigMapRef.Name, app.ConfigMaps) + if err != nil { + return errors.Wrapf(err, "unable to get configMap: %v", e.ConfigMapRef.Name) } - var envs []api_v1.EnvVar + // start populating - for k := range cm.Data { + for configMapDataKey := range rootConfigMapData { // here we are directly referring to the containers // from app.PodSpec.Containers because that is where data // is parsed into so populating that is more valid thing to do envs = append(envs, api_v1.EnvVar{ - Name: k, + Name: configMapDataKey, ValueFrom: &api_v1.EnvVarSource{ ConfigMapKeyRef: &api_v1.ConfigMapKeySelector{ LocalObjectReference: api_v1.LocalObjectReference{ - Name: cmName, + Name: e.ConfigMapRef.Name, }, - Key: k, + Key: configMapDataKey, }, }, }) } - // we collect all the envs from configMap before - // envs provided inside the container - envs = append(envs, app.PodSpec.Containers[ci].Env...) - app.PodSpec.Containers[ci].Env = envs - - cmFound = true - // once the population is done we exit out of the loop - // we don't need to check other configMaps - break } - if !cmFound { - return fmt.Errorf("undefined configMap in app.containers[%d].envFrom[%d].configMapRef.name", ci, ei) + + // populating secrets + if e.SecretRef != nil { + rootSecretDataKeys, err := getSecretDataKeys(e.SecretRef.Name, app.Secrets) + if err != nil { + return errors.Wrap(err, "unable to get secret data") + } + + for _, secretDataKey := range rootSecretDataKeys { + envs = append(envs, api_v1.EnvVar{ + Name: secretDataKey, + ValueFrom: &api_v1.EnvVarSource{ + SecretKeyRef: &api_v1.SecretKeySelector{ + LocalObjectReference: api_v1.LocalObjectReference{ + Name: e.SecretRef.Name, + }, + Key: secretDataKey, + }, + }, + }) + } } + + // we collect all the envs from configMap and secret before + // envs provided inside the container + envs = append(envs, app.PodSpec.Containers[ci].Env...) + app.PodSpec.Containers[ci].Env = envs } } return nil } +func createSecrets(app *spec.App) ([]runtime.Object, error) { + var secrets []runtime.Object + + for _, s := range app.Secrets { + secret := &api_v1.Secret{ + ObjectMeta: api_v1.ObjectMeta{ + Name: s.Name, + Labels: app.Labels, + }, + Data: s.Data, + StringData: s.StringData, + Type: s.Type, + } + secrets = append(secrets, secret) + } + return secrets, nil +} + func CreateK8sObjects(app *spec.App) ([]runtime.Object, error) { var objects []runtime.Object @@ -350,6 +404,11 @@ func CreateK8sObjects(app *spec.App) ([]runtime.Object, error) { return nil, errors.Wrap(err, "Unable to create Kubernetes Ingresses") } + secs, err := createSecrets(app) + if err != nil { + return nil, errors.Wrap(err, "Unable to create Kubernetes Secrets") + } + // withdraw the health and populate actual pod spec if err := populateContainerHealth(app); err != nil { return nil, errors.Wrapf(err, "app %q", app.Name) @@ -423,6 +482,9 @@ func CreateK8sObjects(app *spec.App) ([]runtime.Object, error) { objects = append(objects, pvcs...) log.Debugf("app: %s, pvc: %s\n", app.Name, spew.Sprint(pvcs)) + objects = append(objects, secs...) + log.Debugf("app: %s, secret: %s\n", app.Name, spew.Sprint(secs)) + return objects, nil }