diff --git a/pkg/imagestream/imagestream.go b/pkg/imagestream/imagestream.go new file mode 100644 index 000000000..add0ecdcb --- /dev/null +++ b/pkg/imagestream/imagestream.go @@ -0,0 +1,193 @@ +package imagestream + +import ( + "context" + "fmt" + + "github.com/golang/glog" + "github.com/openshift-kni/eco-goinfra/pkg/clients" + "github.com/openshift-kni/eco-goinfra/pkg/msg" + imagev1 "github.com/openshift/api/image/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + goclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Builder provides a struct for imageStream object from the cluster and a imageStream definition. +type Builder struct { + // imageStream definition, used to create the imageStream object. + Definition *imagev1.ImageStream + // Created imageStream object. + Object *imagev1.ImageStream + // api client to interact with the cluster. + apiClient goclient.Client + // Used in functions that define or mutate clusterOperator definition. errorMsg is processed before the + // ClusterOperator object is created. + errorMsg string +} + +// Pull retrieves an existing imageStream object from the cluster. +func Pull(apiClient *clients.Settings, name, nsname string) (*Builder, error) { + glog.V(100).Infof( + "Pulling imageStream object name %s from namespace %s", name, nsname) + + if apiClient == nil { + glog.V(100).Infof("The apiClient is empty") + + return nil, fmt.Errorf("imageStream 'apiClient' cannot be empty") + } + + err := apiClient.AttachScheme(imagev1.AddToScheme) + if err != nil { + glog.V(100).Info("Failed to add imageStream v1 scheme to client schemes") + + return nil, err + } + + builder := Builder{ + apiClient: apiClient.Client, + Definition: &imagev1.ImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: nsname, + }, + Spec: imagev1.ImageStreamSpec{}, + }, + } + + if name == "" { + glog.V(100).Infof("The name of the imageStream is empty") + + return nil, fmt.Errorf("imageStream 'name' cannot be empty") + } + + if nsname == "" { + glog.V(100).Infof("The namespace of the imageStream is empty") + + return nil, fmt.Errorf("imageStream 'nsname' cannot be empty") + } + + if !builder.Exists() { + return nil, fmt.Errorf("imageStream object %s does not exist in namespace %s", + name, nsname) + } + + builder.Definition = builder.Object + + return &builder, nil +} + +// Get fetches existing imageStream from cluster. +func (builder *Builder) Get() (*imagev1.ImageStream, error) { + if valid, err := builder.validate(); !valid { + return nil, err + } + + glog.V(100).Infof("Getting existing imageStream with name %s in namespace %s from cluster", + builder.Definition.Name, builder.Definition.Namespace) + + imageStreamObj := &imagev1.ImageStream{} + err := builder.apiClient.Get(context.TODO(), goclient.ObjectKey{ + Name: builder.Definition.Name, + Namespace: builder.Definition.Namespace, + }, imageStreamObj) + + if err != nil { + glog.V(100).Infof("imageStream object %s does not exist in namespace %s", + builder.Definition.Name, builder.Definition.Namespace) + + return nil, err + } + + return imageStreamObj, nil +} + +// Exists checks whether the given imageStream exists. +func (builder *Builder) Exists() bool { + if valid, _ := builder.validate(); !valid { + return false + } + + glog.V(100).Infof("Checking if imageStream %s exists in namespace %s", + builder.Definition.Name, builder.Definition.Namespace) + + var err error + builder.Object, err = builder.Get() + + return err == nil || !k8serrors.IsNotFound(err) +} + +// GetDockerImage fetches imageStream DockerImage value. +func (builder *Builder) GetDockerImage(imageTag string) (string, error) { + if valid, err := builder.validate(); !valid { + return "", err + } + + glog.V(100).Infof("Getting imageStream DockerImage value") + + if imageTag == "" { + glog.V(100).Infof("The imageTag of the imageStream is empty") + + return "", fmt.Errorf("imageStream 'imageTag' cannot be empty") + } + + if !builder.Exists() { + return "", fmt.Errorf("imageStream object does not exist") + } + + if len(builder.Object.Spec.Tags) == 0 { + return "", fmt.Errorf("imageStream object %s in namespace %s has no tags", + builder.Definition.Name, builder.Definition.Namespace) + } + + for _, tag := range builder.Object.Spec.Tags { + if tag.From == nil { + return "", fmt.Errorf("imageStream object %s in namespace %s has no DockerImage value", + builder.Definition.Name, builder.Definition.Namespace) + } + + if tag.Name == "" { + return "", fmt.Errorf("imageStream object %s in namespace %s has no DockerImage tag value", + builder.Definition.Name, builder.Definition.Namespace) + } + + if tag.Name == imageTag { + return tag.From.Name, nil + } + } + + return "", fmt.Errorf("image tag %s not found for imageStream object %s in namespace %s", + imageTag, builder.Definition.Name, builder.Definition.Namespace) +} + +// validate will check that the builder and builder definition are properly initialized before +// accessing any member fields. +func (builder *Builder) validate() (bool, error) { + resourceCRD := "ImageStream" + + if builder == nil { + glog.V(100).Infof("The %s builder is uninitialized", resourceCRD) + + return false, fmt.Errorf("error: received nil %s builder", resourceCRD) + } + + if builder.Definition == nil { + glog.V(100).Infof("The %s is undefined", resourceCRD) + + return false, fmt.Errorf(msg.UndefinedCrdObjectErrString(resourceCRD)) + } + + if builder.apiClient == nil { + glog.V(100).Infof("The %s builder apiclient is nil", resourceCRD) + + return false, fmt.Errorf("%s builder cannot have nil apiClient", resourceCRD) + } + + if builder.errorMsg != "" { + glog.V(100).Infof("The %s builder has error message: %s", resourceCRD, builder.errorMsg) + + return false, fmt.Errorf(builder.errorMsg) + } + + return true, nil +} diff --git a/pkg/imagestream/imagestream_test.go b/pkg/imagestream/imagestream_test.go new file mode 100644 index 000000000..3b4dd3eda --- /dev/null +++ b/pkg/imagestream/imagestream_test.go @@ -0,0 +1,383 @@ +package imagestream + +import ( + "fmt" + "testing" + + goclient "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/golang/glog" + "github.com/openshift-kni/eco-goinfra/pkg/clients" + imagev1 "github.com/openshift/api/image/v1" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +var ( + imageStreamName = "network-tools" + imageStreamNamespace = "openshift" + imageTag = "latest" + testSchemes = []clients.SchemeAttacher{ + imagev1.AddToScheme, + } +) + +func TestImageStreamPull(t *testing.T) { + testCases := []struct { + name string + namespace string + addToRuntimeObjects bool + expectedError error + client bool + }{ + { + name: imageStreamName, + namespace: imageStreamNamespace, + addToRuntimeObjects: true, + expectedError: nil, + client: true, + }, + { + name: "", + namespace: imageStreamNamespace, + addToRuntimeObjects: true, + expectedError: fmt.Errorf("imageStream 'name' cannot be empty"), + client: true, + }, + { + name: imageStreamName, + namespace: "", + addToRuntimeObjects: true, + expectedError: fmt.Errorf("imageStream 'nsname' cannot be empty"), + client: true, + }, + { + name: imageStreamName, + namespace: imageStreamNamespace, + addToRuntimeObjects: false, + expectedError: fmt.Errorf("imageStream object %s does not exist in namespace %s", + imageStreamName, imageStreamNamespace), + client: true, + }, + { + name: imageStreamName, + namespace: imageStreamNamespace, + addToRuntimeObjects: true, + expectedError: fmt.Errorf("imageStream 'apiClient' cannot be empty"), + client: false, + }, + } + + for _, testCase := range testCases { + var ( + runtimeObjects []runtime.Object + testSettings *clients.Settings + ) + + testImageStream := buildDummyImageStream(testCase.name, testCase.namespace) + + if testCase.addToRuntimeObjects { + runtimeObjects = append(runtimeObjects, testImageStream) + } + + if testCase.client { + testSettings = clients.GetTestClients(clients.TestClientParams{ + K8sMockObjects: runtimeObjects, + SchemeAttachers: testSchemes, + }) + } + + builderResult, err := Pull(testSettings, testCase.name, testCase.namespace) + assert.Equal(t, testCase.expectedError, err) + + if testCase.expectedError == nil { + assert.Equal(t, testImageStream.Name, builderResult.Object.Name) + assert.Equal(t, testImageStream.Namespace, builderResult.Object.Namespace) + } + } +} + +func TestImageStreamGet(t *testing.T) { + testCases := []struct { + testImageStream *Builder + expectedError string + }{ + { + testImageStream: buildValidImageStreamBuilder(buildImageStreamClientWithDummyObject()), + expectedError: "", + }, + { + testImageStream: buildInValidImageStreamBuilder(buildImageStreamClientWithDummyObject()), + expectedError: "the imageStream 'name' cannot be empty", + }, + { + testImageStream: buildValidImageStreamBuilder(buildTestClientWithImageStreamScheme()), + expectedError: "imagestreams.image.openshift.io \"network-tools\" not found", + }, + } + + for _, testCase := range testCases { + imageStreamObj, err := testCase.testImageStream.Get() + + if testCase.expectedError == "" { + assert.Equal(t, imageStreamObj.Name, testCase.testImageStream.Definition.Name) + assert.Equal(t, imageStreamObj.Namespace, testCase.testImageStream.Definition.Namespace) + assert.Nil(t, err) + } else { + assert.EqualError(t, err, testCase.expectedError) + } + } +} + +func TestImageStreamExists(t *testing.T) { + testCases := []struct { + testImageStream *Builder + expectedStatus bool + }{ + { + testImageStream: buildValidImageStreamBuilder(buildImageStreamClientWithDummyObject()), + expectedStatus: true, + }, + { + testImageStream: buildInValidImageStreamBuilder(buildImageStreamClientWithDummyObject()), + expectedStatus: false, + }, + { + testImageStream: buildValidImageStreamBuilder(buildTestClientWithImageStreamScheme()), + expectedStatus: false, + }, + } + + for _, testCase := range testCases { + exist := testCase.testImageStream.Exists() + assert.Equal(t, testCase.expectedStatus, exist) + } +} + +//nolint:funlen +func TestImageStreamGetDockerImage(t *testing.T) { + testCases := []struct { + testImageStreamTags []imagev1.TagReference + testImageTag string + expectedError error + }{ + { + testImageStreamTags: []imagev1.TagReference{{ + Name: "latest", + From: &corev1.ObjectReference{ + Kind: "DockerImage", + Name: "quay.io/dummy-server/ocp-v4.0-art-dev@sha256:c8bc1d7bdf77538653ff1cd40d9bfa00f0", + }, + }}, + testImageTag: imageTag, + expectedError: nil, + }, + { + testImageStreamTags: []imagev1.TagReference{{ + Name: "7.4.0", + From: &corev1.ObjectReference{ + Kind: "DockerImage", + Name: "registry.dummy.io/jboss-eap-7/eap74-openjdk8-openshift-rhel7:latest", + }}, { + Name: "latest", + From: &corev1.ObjectReference{ + Kind: "DockerImage", + Name: "registry.dummy.io/jboss-eap-7/eap74-openjdk8-openshift-rhel7:latest", + }, + }}, + testImageTag: imageTag, + expectedError: nil, + }, + { + testImageStreamTags: []imagev1.TagReference{}, + testImageTag: imageTag, + expectedError: fmt.Errorf("imageStream object network-tools in namespace openshift has no tags"), + }, + { + testImageStreamTags: []imagev1.TagReference{{ + Name: "latest", + From: &corev1.ObjectReference{ + Kind: "DockerImage", + Name: "quay.io/dummy-server/ocp-v4.0-art-dev@sha256:c8bc1d7bdf77538653ff1cd40d9bfa00f0", + }, + }}, + testImageTag: "4.16", + expectedError: fmt.Errorf("image tag 4.16 not found for imageStream object network-tools " + + "in namespace openshift"), + }, + { + testImageStreamTags: []imagev1.TagReference{{ + Name: "", + From: &corev1.ObjectReference{ + Kind: "DockerImage", + Name: "quay.io/dummy-server/ocp-v4.0-art-dev@sha256:c8bc1d7bdf77538653ff1cd40d9bfa00f0", + }, + }}, + testImageTag: imageTag, + expectedError: fmt.Errorf("imageStream object network-tools in namespace openshift has no DockerImage tag value"), + }, + { + testImageStreamTags: []imagev1.TagReference{{ + Name: "latest", + From: nil, + }}, + testImageTag: imageTag, + expectedError: fmt.Errorf("imageStream object network-tools in namespace openshift has no DockerImage value"), + }, + { + testImageStreamTags: []imagev1.TagReference{{ + Name: "latest", + From: &corev1.ObjectReference{ + Kind: "DockerImage", + Name: "quay.io/dummy-server/ocp-v4.0-art-dev@sha256:c8bc1d7bdf77538653ff1cd40d9bfa00f0", + }, + }}, + testImageTag: "", + expectedError: fmt.Errorf("imageStream 'imageTag' cannot be empty"), + }, + } + + for _, testCase := range testCases { + var runtimeObjects []runtime.Object + + testImageStreamClientWithDummyObject := buildDummyImageStreamWithTag(testCase.testImageStreamTags) + + runtimeObjects = append(runtimeObjects, testImageStreamClientWithDummyObject) + + dummyImageStream := clients.GetTestClients(clients.TestClientParams{ + K8sMockObjects: runtimeObjects, + SchemeAttachers: testSchemes, + }) + + testImageStreamBuilder := buildValidImageStreamBuilder(dummyImageStream) + + dockerImage, err := testImageStreamBuilder.GetDockerImage(testCase.testImageTag) + assert.Equal(t, testCase.expectedError, err) + + if testCase.expectedError == nil { + assert.NotEqual(t, "", dockerImage) + } else { + assert.Equal(t, "", dockerImage) + } + } +} + +func buildValidImageStreamBuilder(apiClient *clients.Settings) *Builder { + imageStreamBuilder := newBuilder( + apiClient, imageStreamName, imageStreamNamespace) + + return imageStreamBuilder +} + +func buildInValidImageStreamBuilder(apiClient *clients.Settings) *Builder { + imageStreamBuilder := newBuilder( + apiClient, "", imageStreamNamespace) + + return imageStreamBuilder +} + +func buildImageStreamClientWithDummyObject() *clients.Settings { + return clients.GetTestClients(clients.TestClientParams{ + K8sMockObjects: []runtime.Object{ + buildDummyImageStream(imageStreamName, imageStreamNamespace), + }, + SchemeAttachers: testSchemes, + }) +} + +// buildImageStreamClientWithScheme returns a client with no objects but the ImageStream scheme attached. +func buildTestClientWithImageStreamScheme() *clients.Settings { + return clients.GetTestClients(clients.TestClientParams{ + SchemeAttachers: testSchemes, + }) +} + +func buildDummyImageStream(name, namespace string) *imagev1.ImageStream { + return &imagev1.ImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: imagev1.ImageStreamSpec{ + LookupPolicy: imagev1.ImageLookupPolicy{ + Local: false, + }, + Tags: []imagev1.TagReference{{ + Name: "latest", + From: &corev1.ObjectReference{ + Kind: "DockerImage", + Name: "quay.io/dummy-server/ocp-v4.0-art-dev@sha256:c8bc1d7bdf77538653ff1cd40d9bfa00f0", + }, + }}, + }, + } +} + +func buildDummyImageStreamWithTag(imagestreamTags []imagev1.TagReference) *imagev1.ImageStream { + return &imagev1.ImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: imageStreamName, + Namespace: imageStreamNamespace, + }, + Spec: imagev1.ImageStreamSpec{ + LookupPolicy: imagev1.ImageLookupPolicy{ + Local: false, + }, + Tags: imagestreamTags, + }, + } +} + +// newBuilder method creates new instance of builder (for the unit test propose only). +func newBuilder(apiClient *clients.Settings, name, namespace string) *Builder { + glog.V(100).Infof("Initializing new Builder structure with the name %s in namespace %s", + name, namespace) + + var client goclient.Client + + if apiClient != nil { + client = apiClient.Client + } + + builder := &Builder{ + apiClient: client, + Definition: &imagev1.ImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: imagev1.ImageStreamSpec{ + LookupPolicy: imagev1.ImageLookupPolicy{ + Local: false, + }, + Tags: []imagev1.TagReference{{ + Name: "latest", + From: &corev1.ObjectReference{ + Kind: "DockerImage", + Name: "quay.io/dummy-server/ocp-v4.0-art-dev@sha256:c8bc1d7bdf77538653ff1cd40d9bfa00f0", + }, + }}, + }, + }, + } + + if name == "" { + glog.V(100).Infof("The name of the imageStream is empty") + + builder.errorMsg = "the imageStream 'name' cannot be empty" + + return builder + } + + if namespace == "" { + glog.V(100).Infof("The namespace of the imageStream is empty") + + builder.errorMsg = "the imageStream 'name' cannot be empty" + + return builder + } + + return builder +}