From 83b819c2a55bd147abeb5cdb0b53a703ac506e04 Mon Sep 17 00:00:00 2001 From: Shubham Minglani Date: Mon, 13 Nov 2017 20:49:38 +0530 Subject: [PATCH] add ImageStreams support This commit adds ImageStream support to the Kedge spec. A user will be able to define an ImageStream object using the imageStreams key at the root level of the spec, which is created by merging the ImageStreamSpec and ObjectMeta. This commit also includes relevant docs, tests, and examples. --- docs/examples/imagestreams/README.md | 17 +++ docs/examples/imagestreams/is.yml | 22 ++++ docs/file-reference.md | 65 +++++++++++ pkg/cmd/commands.go | 2 +- pkg/spec/resources.go | 48 ++++++++ pkg/spec/resources_test.go | 169 +++++++++++++++++++++++++++ pkg/spec/types.go | 13 +++ pkg/spec/util.go | 6 + 8 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 docs/examples/imagestreams/README.md create mode 100644 docs/examples/imagestreams/is.yml diff --git a/docs/examples/imagestreams/README.md b/docs/examples/imagestreams/README.md new file mode 100644 index 000000000..5dcf53729 --- /dev/null +++ b/docs/examples/imagestreams/README.md @@ -0,0 +1,17 @@ +# Image Streams + +An image stream can be used to automatically perform an action, such as updating a deployment when a new image is created. + +Like most of the Kedge constructs, the `ImageStreamSpec` and `ObjectMeta` have been merged at the same YAML level. + +A valid OpenShift `ImageStream` resource can be specified at the root level of the Kedge spec in a field called `imageStreams` like we see in [is.yml](is.yml): + +```yaml +name: webapp +imageStreams: +- tags: + - from: + kind: DockerImage + name: centos/httpd-24-centos7:2.4 + name: "2.4" +``` diff --git a/docs/examples/imagestreams/is.yml b/docs/examples/imagestreams/is.yml new file mode 100644 index 000000000..6806fda70 --- /dev/null +++ b/docs/examples/imagestreams/is.yml @@ -0,0 +1,22 @@ +controller: DeploymentConfig +name: webapp +containers: +- image: "" +triggers: +- imageChangeParams: + automatic: true + containerNames: + - webapp + from: + kind: ImageStreamTag + name: webapp:2.4 + type: ImageChange +services: +- portMappings: + - "8080" +imageStreams: +- tags: + - from: + kind: DockerImage + name: centos/httpd-24-centos7:2.4 + name: "2.4" diff --git a/docs/file-reference.md b/docs/file-reference.md index 0367f7928..cb8685920 100644 --- a/docs/file-reference.md +++ b/docs/file-reference.md @@ -118,6 +118,8 @@ routes: - secrets: - +imageStreams: + - includeResources: - ``` @@ -141,6 +143,7 @@ Each "app" (Kedge file) is a Kubernetes +Each "imageStreamObject" is an OpenShift ImageStream Spec with additional Kedge-specific keys. + + + +| Type | Required | Description | +|----------------------------------|--------------|------| +| name | string | yes | The name of the ImageStream | + + +### name + +```yaml +name: wordpress +``` + +| Type | Required | Description | +|----------|--------------|-------| +| string | yes | The name of the ImageStream | + +### OpenShift extension + +> Example extending `imageStreams` with OpenShift ImageStream Spec + +```yaml +name: webapp +imageStreams: +- tags: + - from: + kind: DockerImage + name: centos/httpd-24-centos7:latest + name: "2.4" +``` + ## includeResources ```yaml diff --git a/pkg/cmd/commands.go b/pkg/cmd/commands.go index 467cf4a8e..c777e1a17 100644 --- a/pkg/cmd/commands.go +++ b/pkg/cmd/commands.go @@ -64,7 +64,7 @@ func CreateArtifacts(paths []string, generate bool, args ...string) error { for _, runtimeObject := range ros { switch runtimeObject.GetObjectKind().GroupVersionKind().Kind { // If there is at least one OpenShift resource use oc - case "DeploymentConfig", "Route": + case "DeploymentConfig", "Route", "ImageStream": useOC = true break } diff --git a/pkg/spec/resources.go b/pkg/spec/resources.go index 4ac81b6b5..ee09b22ea 100644 --- a/pkg/spec/resources.go +++ b/pkg/spec/resources.go @@ -22,6 +22,7 @@ import ( "strings" log "github.com/Sirupsen/logrus" + image_v1 "github.com/openshift/origin/pkg/image/apis/image/v1" os_route_v1 "github.com/openshift/origin/pkg/route/apis/route/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" api_v1 "k8s.io/kubernetes/pkg/api/v1" @@ -125,6 +126,26 @@ func fixSecrets(secrets []SecretMod, appName string) ([]SecretMod, error) { return secrets, nil } +func fixImageStreams(imageStreams []ImageStreamSpecMod, appName string) ([]ImageStreamSpecMod, error) { + + // auto populate name only if one ImageStream is specified without any name + if len(imageStreams) == 1 && imageStreams[0].Name == "" { + imageStreams[0].ObjectMeta.Name = appName + } + + for i, is := range imageStreams { + if is.Name == "" { + return nil, fmt.Errorf("please specify name for app.imageStreams[%d]", i) + } + + is.ObjectMeta.Labels = addKeyValueToMap(appLabelKey, appName, is.ObjectMeta.Labels) + + // this should be the last statement in this for loop + imageStreams[i] = is + } + return imageStreams, nil +} + func fixIngresses(ingresses []IngressSpecMod, appName string) ([]IngressSpecMod, error) { // auto populate name only if one ingress is specified without any name @@ -217,6 +238,12 @@ func (cf *ControllerFields) fixControllerFields() error { return errors.Wrap(err, "unable to fix secrets") } + // fix imageStreams + cf.ImageStreams, err = fixImageStreams(cf.ImageStreams, cf.Name) + if err != nil { + return errors.Wrap(err, "unable to fix imageStreams") + } + // fix ingresses cf.Ingresses, err = fixIngresses(cf.Ingresses, cf.Name) if err != nil { @@ -400,6 +427,19 @@ func (app *ControllerFields) createSecrets() ([]runtime.Object, error) { return secrets, nil } +func (app *ControllerFields) createImageStreams() ([]runtime.Object, error) { + var imageStreams []runtime.Object + + for _, is := range app.ImageStreams { + imageStream := &image_v1.ImageStream{ + ObjectMeta: is.ObjectMeta, + Spec: is.ImageStreamSpec, + } + imageStreams = append(imageStreams, imageStream) + } + return imageStreams, nil +} + // CreateK8sObjects, if given object DeploymentSpecMod, this function reads // them and returns kubernetes objects as list of runtime.Object // If the deployment is using field 'includeResources' then it will @@ -431,6 +471,11 @@ func (app *ControllerFields) CreateK8sObjects() ([]runtime.Object, []string, err return nil, nil, errors.Wrap(err, "Unable to create Kubernetes Secrets") } + iss, err := app.createImageStreams() + if err != nil { + return nil, nil, errors.Wrap(err, "unable to create OpenShift ImageStreams") + } + app.PodSpec.Containers, err = populateContainers(app.Containers, app.ConfigMaps, app.Secrets) if err != nil { return nil, nil, errors.Wrapf(err, "deployment %q", app.Name) @@ -486,6 +531,9 @@ func (app *ControllerFields) CreateK8sObjects() ([]runtime.Object, []string, err objects = append(objects, secs...) log.Debugf("app: %s, secret: %s\n", app.Name, spew.Sprint(secs)) + objects = append(objects, iss...) + log.Debugf("app: %s, imageStreams: %s\n", app.Name, spew.Sprint(iss)) + objects = append(objects, configMap...) log.Debugf("app: %s, configMap: %s\n", app.Name, spew.Sprint(configMap)) diff --git a/pkg/spec/resources_test.go b/pkg/spec/resources_test.go index b64900893..871434a3b 100644 --- a/pkg/spec/resources_test.go +++ b/pkg/spec/resources_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/davecgh/go-spew/spew" + image_v1 "github.com/openshift/origin/pkg/image/apis/image/v1" os_route_v1 "github.com/openshift/origin/pkg/route/apis/route/v1" meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -170,6 +171,85 @@ func TestFixSecrets(t *testing.T) { } } +func TestFixImageStreams(t *testing.T) { + appName := "testAppName" + tests := []struct { + name string + input []ImageStreamSpecMod + output []ImageStreamSpecMod + success bool + }{ + { + name: "passing one imageStream without name", + input: []ImageStreamSpecMod{ + {}, + }, + output: []ImageStreamSpecMod{ + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: appName, + Labels: map[string]string{ + appLabelKey: appName, + }, + }, + }, + }, + success: true, + }, + { + name: "passing one imageStream with name", + input: []ImageStreamSpecMod{ + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "imageStreamName", + }, + }, + }, + output: []ImageStreamSpecMod{ + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "imageStreamName", + Labels: map[string]string{ + appLabelKey: appName, + }, + }, + }, + }, + success: true, + }, + { + name: "passing multiple ingresses without names", + input: []ImageStreamSpecMod{ + {}, + {}, + }, + output: nil, + success: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fixedImageStreams, err := fixImageStreams(test.input, appName) + + switch test.success { + case true: + if err != nil { + t.Errorf("Expected test to pass but got an error -\n%v", err) + } + case false: + if err == nil { + t.Errorf("For the input -\n%v\nexpected test to fail, but test passed", prettyPrintObjects(test.input)) + } + } + + if !reflect.DeepEqual(fixedImageStreams, test.output) { + t.Errorf("Expected fixed imageStreams to be -\n%v\nBut got -\n%v\n", prettyPrintObjects(test.output), prettyPrintObjects(fixedImageStreams)) + } + }) + } +} + func TestFixIngresses(t *testing.T) { appName := "testAppName" tests := []struct { @@ -627,6 +707,95 @@ func TestCreateServices(t *testing.T) { } } +func TestCreateImageStreams(t *testing.T) { + tests := []struct { + name string + input *ControllerFields + output []runtime.Object + }{ + { + name: "no imageStreams passed", + input: &ControllerFields{}, + output: nil, + }, + { + name: "passing 1 imageStream definition", + input: &ControllerFields{ + ImageStreams: []ImageStreamSpecMod{ + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "testIS", + }, + ImageStreamSpec: image_v1.ImageStreamSpec{ + DockerImageRepository: "testRepo", + }, + }, + }, + }, + output: []runtime.Object{ + &image_v1.ImageStream{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "testIS", + }, + Spec: image_v1.ImageStreamSpec{ + DockerImageRepository: "testRepo", + }}, + }, + }, + { + name: "passing 2 imageStream definitions", + input: &ControllerFields{ + ImageStreams: []ImageStreamSpecMod{ + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "testIS1", + }, + ImageStreamSpec: image_v1.ImageStreamSpec{ + DockerImageRepository: "testRepo1", + }, + }, + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "testIS2", + }, + ImageStreamSpec: image_v1.ImageStreamSpec{ + DockerImageRepository: "testRepo2", + }, + }, + }, + }, + output: []runtime.Object{ + &image_v1.ImageStream{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "testIS1", + }, + Spec: image_v1.ImageStreamSpec{ + DockerImageRepository: "testRepo1", + }}, + &image_v1.ImageStream{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "testIS2", + }, + Spec: image_v1.ImageStreamSpec{ + DockerImageRepository: "testRepo2", + }}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + objects, err := test.input.createImageStreams() + if err != nil { + t.Errorf("Creating imageStreams failed: %v", err) + } + if !reflect.DeepEqual(test.output, objects) { + t.Fatalf("Expected:\n%v\nGot:\n%v", prettyPrintObjects(test.output), prettyPrintObjects(objects)) + } + }) + } +} + func TestParsePortMapping(t *testing.T) { tests := []struct { name string diff --git a/pkg/spec/types.go b/pkg/spec/types.go index 329c65bf8..6889f4175 100644 --- a/pkg/spec/types.go +++ b/pkg/spec/types.go @@ -18,6 +18,7 @@ package spec import ( os_deploy_v1 "github.com/openshift/origin/pkg/deploy/apis/apps/v1" + image_v1 "github.com/openshift/origin/pkg/image/apis/image/v1" os_route_v1 "github.com/openshift/origin/pkg/route/apis/route/v1" meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" api_v1 "k8s.io/kubernetes/pkg/api/v1" @@ -142,6 +143,14 @@ type SecretMod struct { meta_v1.ObjectMeta `json:",inline"` } +// ImageStreamSpec defines OpenShift ImageStream Object +// kedgeSpec: io.kedge.ImageStreamSpec +type ImageStreamSpecMod struct { + image_v1.ImageStreamSpec `json:",inline"` + // k8s: io.k8s.kubernetes.pkg.apis.meta.v1.ObjectMeta + meta_v1.ObjectMeta `json:",inline"` +} + // ControllerFields are the common fields in every controller Kedge supports type ControllerFields struct { Controller string `json:"controller,omitempty"` @@ -169,6 +178,10 @@ type ControllerFields struct { // ref: io.kedge.SecretSpec // +optional Secrets []SecretMod `json:"secrets,omitempty"` + // List of OpenShift ImageStreams + // ref: io.kedge.ImageStreamSpec + // +optional + ImageStreams []ImageStreamSpecMod `json:"imageStreams,omitempty"` // List of Kubernetes resource files, that can be directly given to Kubernetes // +optional IncludeResources []string `json:"includeResources,omitempty"` diff --git a/pkg/spec/util.go b/pkg/spec/util.go index 7149e678e..16ee92d1c 100644 --- a/pkg/spec/util.go +++ b/pkg/spec/util.go @@ -29,6 +29,7 @@ import ( api_v1 "k8s.io/kubernetes/pkg/api/v1" //kapi "k8s.io/kubernetes/pkg/api/v1" os_deploy_v1 "github.com/openshift/origin/pkg/deploy/apis/apps/v1" + image_v1 "github.com/openshift/origin/pkg/image/apis/image/v1" os_route_v1 "github.com/openshift/origin/pkg/route/apis/route/v1" batch_v1 "k8s.io/kubernetes/pkg/apis/batch/v1" ) @@ -86,6 +87,11 @@ func GetScheme() (*runtime.Scheme, error) { return nil, errors.Wrap(err, "unable to add 'routes' to scheme") } + // Adding the image scheme to support OpenShift ImageStreams + if err := image_v1.AddToScheme(scheme); err != nil { + return nil, errors.Wrap(err, "unable to add 'image' to scheme") + } + return scheme, nil }