diff --git a/.travis.yml b/.travis.yml index f571f24b9d6..8287bf68588 100644 --- a/.travis.yml +++ b/.travis.yml @@ -142,6 +142,19 @@ jobs: # - travis_wait make test-cmd-docker-devfile-url-pushtarget # These tests need cluster login as they will be interacting with a Kube environment - odo login -u developer + - travis_wait make test-cmd-devfile-catalog + - travis_wait make test-cmd-devfile-create + - travis_wait make test-cmd-devfile-push + - travis_wait make test-cmd-devfile-watch + - travis_wait make test-cmd-devfile-delete + - travis_wait make test-cmd-devfile-deploy + - travis_wait make test-cmd-devfile-deploy-delete + - travis_wait make test-cmd-devfile-registry + - travis_wait make test-cmd-devfile-test + - travis_wait make test-cmd-devfile-log + - travis_wait make test-cmd-devfile-exec + - travis_wait make test-cmd-devfile-env + - travis_wait make test-cmd-devfile-config - travis_wait 60 make test-integration-devfile - odo logout @@ -204,4 +217,4 @@ jobs: - sudo cp odo /usr/bin - export KUBERNETES=true - travis_wait make test-cmd-project - - travis_wait 60 make test-integration-devfile \ No newline at end of file + - travis_wait 60 make test-integration-devfile diff --git a/Makefile b/Makefile index ae8f5bf2efb..686a13fc71a 100644 --- a/Makefile +++ b/Makefile @@ -225,6 +225,16 @@ test-cmd-devfile-create: test-cmd-devfile-push: ginkgo $(GINKGO_FLAGS) -focus="odo devfile push command tests" tests/integration/devfile/ +# Run odo deploy devfile command tests +.PHONY: test-cmd-devfile-deploy +test-cmd-devfile-deploy: + ginkgo $(GINKGO_FLAGS) -focus="odo devfile deploy command tests" tests/integration/devfile/ + +# Run odo deploy delete devfile command tests +.PHONY: test-cmd-devfile-deploy-delete +test-cmd-devfile-deploy-delete: + ginkgo $(GINKGO_FLAGS) -focus="odo devfile deploy delete command tests" tests/integration/devfile/ + # Run odo exec devfile command tests .PHONY: test-cmd-devfile-exec test-cmd-devfile-exec: diff --git a/pkg/devfile/adapters/common/interface.go b/pkg/devfile/adapters/common/interface.go index 2b67b3ccd9e..228238e4737 100644 --- a/pkg/devfile/adapters/common/interface.go +++ b/pkg/devfile/adapters/common/interface.go @@ -8,7 +8,10 @@ import ( type ComponentAdapter interface { commandExecutor Push(parameters PushParameters) error + Build(parameters BuildParameters) error DoesComponentExist(cmpName string) (bool, error) + Deploy(parameters DeployParameters) error + DeployDelete(manifest []byte) error Delete(labels map[string]string, show bool) error Test(testCmd string, show bool) error Log(follow, debug bool) (io.ReadCloser, error) diff --git a/pkg/devfile/adapters/common/types.go b/pkg/devfile/adapters/common/types.go index 0ca5b81c1e0..c8fec5eea5b 100644 --- a/pkg/devfile/adapters/common/types.go +++ b/pkg/devfile/adapters/common/types.go @@ -27,6 +27,23 @@ type Storage struct { Volume DevfileVolume } +// BuildParameters is a struct containing the parameters to be used when building the image for a devfile component +type BuildParameters struct { + Path string // Path refers to the parent folder containing the source code to push up to a component + DockerfileBytes []byte // DockerfileBytes is the contents of the project Dockerfile in bytes + EnvSpecificInfo envinfo.EnvSpecificInfo // EnvSpecificInfo contains infomation of env.yaml file + Tag string // Tag refers to the image tag of the image being built + IgnoredFiles []string // IgnoredFiles is the list of files to not push up to a component +} + +// DeployParameters is a struct containing the parameters to be used when deploying the image for a devfile component +type DeployParameters struct { + EnvSpecificInfo envinfo.EnvSpecificInfo // EnvSpecificInfo contains infomation of env.yaml file + Tag string // Tag refers to the image tag of the image being built + ManifestSource []byte // Source of the manifest file + DeploymentPort int // Port to be used in deployment manifest +} + // PushParameters is a struct containing the parameters to be used when pushing to a devfile component type PushParameters struct { Path string // Path refers to the parent folder containing the source code to push up to a component diff --git a/pkg/devfile/adapters/docker/adapter.go b/pkg/devfile/adapters/docker/adapter.go index 4f9b8bf4c64..55588203bfb 100644 --- a/pkg/devfile/adapters/docker/adapter.go +++ b/pkg/devfile/adapters/docker/adapter.go @@ -37,6 +37,18 @@ func (d Adapter) Push(parameters common.PushParameters) error { return nil } +func (k Adapter) Build(parameters common.BuildParameters) error { + return errors.New("Deploy command not supported when building image using pushTarget=Docker") +} + +func (k Adapter) Deploy(parameters common.DeployParameters) error { + return errors.New("Deploy command not supported when using pushTarget=Docker") +} + +func (k Adapter) DeployDelete(manifest []byte) error { + return errors.New("Deploy delete command not supported when using pushTarget=Docker") +} + // DoesComponentExist returns true if a component with the specified name exists func (d Adapter) DoesComponentExist(cmpName string) (bool, error) { return d.componentAdapter.DoesComponentExist(cmpName) diff --git a/pkg/devfile/adapters/docker/component/adapter.go b/pkg/devfile/adapters/docker/component/adapter.go index c244d9ea78f..c110cd45154 100644 --- a/pkg/devfile/adapters/docker/component/adapter.go +++ b/pkg/devfile/adapters/docker/component/adapter.go @@ -82,6 +82,12 @@ func (a Adapter) SupervisorComponentInfo(command versionsCommon.DevfileCommand) return common.ComponentInfo{}, nil } +func (a Adapter) Build(parameters common.BuildParameters) (err error) { return nil } + +func (a Adapter) Deploy(parameters common.DeployParameters) (err error) { return nil } + +func (a Adapter) DeployDelete(manifest []byte) (err error) { return nil } + // Push updates the component if a matching component exists or creates one if it doesn't exist func (a Adapter) Push(parameters common.PushParameters) (err error) { componentExists, err := utils.ComponentExists(a.Client, a.Devfile.Data, a.ComponentName) diff --git a/pkg/devfile/adapters/kubernetes/adapter.go b/pkg/devfile/adapters/kubernetes/adapter.go index 64ede30456f..f85e8779854 100644 --- a/pkg/devfile/adapters/kubernetes/adapter.go +++ b/pkg/devfile/adapters/kubernetes/adapter.go @@ -41,6 +41,39 @@ func (k Adapter) Push(parameters common.PushParameters) error { return nil } +// Build creates Kubernetes resources to build an image for the component +func (k Adapter) Build(parameters common.BuildParameters) error { + + err := k.componentAdapter.Build(parameters) + if err != nil { + return errors.Wrap(err, "Failed to build image for the component") + } + + return nil +} + +// Build creates Kubernetes resources to build an image for the component +func (k Adapter) Deploy(parameters common.DeployParameters) error { + + err := k.componentAdapter.Deploy(parameters) + if err != nil { + return errors.Wrap(err, "Failed to deploy the application") + } + + return nil +} + +// Build creates Kubernetes resources to build an image for the component +func (k Adapter) DeployDelete(manifest []byte) error { + + err := k.componentAdapter.DeployDelete(manifest) + if err != nil { + return errors.Wrap(err, "Failed to delete the deployed application") + } + + return nil +} + // DoesComponentExist returns true if a component with the specified name exists func (k Adapter) DoesComponentExist(cmpName string) (bool, error) { return k.componentAdapter.DoesComponentExist(cmpName) diff --git a/pkg/devfile/adapters/kubernetes/component/adapter.go b/pkg/devfile/adapters/kubernetes/component/adapter.go index 15bff326f9f..874a7ed9949 100644 --- a/pkg/devfile/adapters/kubernetes/component/adapter.go +++ b/pkg/devfile/adapters/kubernetes/component/adapter.go @@ -1,10 +1,19 @@ package component import ( + "bufio" + "bytes" "fmt" "io" + "os" + "os/signal" + "path/filepath" "reflect" + "strconv" "strings" + "syscall" + "text/template" + "time" componentlabels "github.com/openshift/odo/pkg/component/labels" "github.com/openshift/odo/pkg/envinfo" @@ -12,10 +21,15 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer/yaml" + "github.com/fatih/color" "github.com/pkg/errors" "k8s.io/klog" + imagev1 "github.com/openshift/api/image/v1" "github.com/openshift/odo/pkg/component" "github.com/openshift/odo/pkg/config" "github.com/openshift/odo/pkg/devfile/adapters/common" @@ -24,12 +38,18 @@ import ( versionsCommon "github.com/openshift/odo/pkg/devfile/parser/data/common" "github.com/openshift/odo/pkg/kclient" "github.com/openshift/odo/pkg/log" + "github.com/openshift/odo/pkg/occlient" odoutil "github.com/openshift/odo/pkg/odo/util" "github.com/openshift/odo/pkg/sync" kerrors "k8s.io/apimachinery/pkg/api/errors" ) -// New instantiates a component adapter +const ( + DeployComponentSuffix = "-deploy" + BuildTimeout = 5 * time.Minute +) + +// New instantiantes a component adapter func New(adapterContext common.AdapterContext, client kclient.Client) Adapter { adapter := Adapter{Client: client} adapter.GenericAdapter = common.NewGenericAdapter(&client, adapterContext) @@ -95,6 +115,348 @@ type Adapter struct { pod *corev1.Pod } +const dockerfilePath string = "Dockerfile" + +func (a Adapter) runBuildConfig(client *occlient.Client, parameters common.BuildParameters) (err error) { + buildName := a.ComponentName + + commonObjectMeta := metav1.ObjectMeta{ + Name: buildName, + } + + buildOutput := "DockerImage" + + if parameters.Tag == "" { + parameters.Tag = fmt.Sprintf("%s:latest", buildName) + buildOutput = "ImageStreamTag" + } + + controlC := make(chan os.Signal, 1) + signal.Notify(controlC, os.Interrupt, syscall.SIGTERM) + go a.terminateBuild(controlC, client, commonObjectMeta) + + _, err = client.CreateDockerBuildConfigWithBinaryInput(commonObjectMeta, dockerfilePath, parameters.Tag, []corev1.EnvVar{}, buildOutput) + if err != nil { + return err + } + + defer func() { + // This will delete both the BuildConfig and any builds using that BuildConfig + derr := client.DeleteBuildConfig(commonObjectMeta) + if err == nil { + err = derr + } + }() + + syncAdapter := sync.New(a.AdapterContext, &a.Client) + reader, err := syncAdapter.SyncFilesBuild(parameters, dockerfilePath) + if err != nil { + return err + } + + bc, err := client.RunBuildConfigWithBinaryInput(buildName, reader) + if err != nil { + return err + } + log.Successf("Started build %s using BuildConfig", bc.Name) + + reader, writer := io.Pipe() + + var cmdOutput string + // This Go routine will automatically pipe the output from WaitForBuildToFinish to + // our logger. + // We pass the controlC os.Signal in order to output the logs within the terminateBuild + // function if the process is interrupted by the user performing a ^C. If we didn't pass it + // The Scanner would consume the log, and only output it if there was an err within this + // func. + go func(controlC chan os.Signal) { + select { + case <-controlC: + return + default: + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + line := scanner.Text() + + if log.IsDebug() { + _, err := fmt.Fprintln(os.Stdout, line) + if err != nil { + log.Errorf("Unable to print to stdout: %v", err) + } + } + + cmdOutput += fmt.Sprintln(line) + } + } + }(controlC) + + s := log.Spinner("Waiting for build to complete") + if err := client.WaitForBuildToFinish(bc.Name, writer, BuildTimeout); err != nil { + s.End(false) + return errors.Wrapf(err, "unable to build image using BuildConfig %s, error: %s", buildName, cmdOutput) + } + + s.End(true) + // Stop listening for a ^C so it doesnt perform terminateBuild during any later stages + signal.Stop(controlC) + log.Successf("Successfully built container image: %s", parameters.Tag) + return +} + +// terminateBuild is triggered if the user performs a ^C action within the terminal during the build phase +// of the deploy. +// It cleans up the resources created for the build, as the defer function would not be reached. +// The subsequent deploy would fail if these resources are not cleaned up. +func (a Adapter) terminateBuild(c chan os.Signal, client *occlient.Client, commonObjectMeta metav1.ObjectMeta) { + <-c + + log.Info("\nBuild process interrupted, terminating build, this might take a few seconds") + err := client.DeleteBuildConfig(commonObjectMeta) + if err != nil { + log.Info("\n", err.Error()) + } + os.Exit(0) +} + +// Build image for devfile project +func (a Adapter) Build(parameters common.BuildParameters) (err error) { + // TODO: set namespace from user flag + client, err := occlient.New() + if err != nil { + return err + } + + isBuildConfigSupported, err := client.IsBuildConfigSupported() + if err != nil { + return err + } + + if isBuildConfigSupported { + return a.runBuildConfig(client, parameters) + } + + return errors.New("unable to build image, only Openshift BuildConfig build is supported") +} + +// Perform the substitutions in the manifest file(s) +func substitueYamlVariables(baseYaml []byte, yamlSubstitutions map[string]string) ([]byte, error) { + // create new template from parsing file + tmpl, err := template.New("deploy").Parse(string(baseYaml)) + if err != nil { + return []byte{}, errors.Wrap(err, "error creating template") + } + + // define a buffer to store the results + var buf bytes.Buffer + + // apply template to yaml file + _ = tmpl.Execute(&buf, yamlSubstitutions) + + return buf.Bytes(), nil +} + +// Build image for devfile project +func (a Adapter) Deploy(parameters common.DeployParameters) (err error) { + // TODO: Can we use a occlient created somewhere else rather than create another + client, err := occlient.New() + if err != nil { + return err + } + + namespace := a.Client.Namespace + applicationName := a.ComponentName + DeployComponentSuffix + deploymentManifest := &unstructured.Unstructured{} + + var imageStream *imagev1.ImageStream + if parameters.Tag == "" { + imageStream, err = client.GetImageStream(namespace, a.ComponentName, "latest") + if err != nil { + return err + } + + imageStreamImage, err := client.GetImageStreamImage(imageStream, "latest") + if err != nil { + return err + } + parameters.Tag = imageStreamImage.Image.DockerImageReference + } + + // Specify the substitution keys and values + yamlSubstitutions := map[string]string{ + "CONTAINER_IMAGE": parameters.Tag, + "COMPONENT_NAME": applicationName, + "PORT": strconv.Itoa(parameters.DeploymentPort), + } + + // Build a yaml decoder with the unstructured Scheme + yamlDecoder := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + + // This will override if manifest.yaml is present + writtenToManifest := false + manifestFile, err := os.Create(filepath.Join(a.Context, ".odo", "manifest.yaml")) + if err != nil { + err = manifestFile.Close() + return errors.Wrap(err, "Unable to create the local manifest file") + } + + defer func() { + merr := manifestFile.Close() + if err == nil { + err = merr + } + }() + + manifests := bytes.Split(parameters.ManifestSource, []byte("---")) + for _, manifest := range manifests { + if len(manifest) > 0 { + // Substitute the values in the manifest file + deployYaml, err := substitueYamlVariables(manifest, yamlSubstitutions) + if err != nil { + return errors.Wrap(err, "unable to substitute variables in manifest") + } + + _, gvk, err := yamlDecoder.Decode([]byte(deployYaml), nil, deploymentManifest) + if err != nil { + return errors.New("Failed to decode the manifest yaml") + } + + kind := utils.PluraliseKind(gvk.Kind) + gvr := schema.GroupVersionResource{Group: gvk.Group, Version: gvk.Version, Resource: kind} + klog.V(3).Infof("Manifest type: %s", gvr.String()) + + labels := map[string]string{ + "component": applicationName, + } + + manifestLabels := deploymentManifest.GetLabels() + if manifestLabels != nil { + for key, value := range labels { + manifestLabels[key] = value + } + deploymentManifest.SetLabels(manifestLabels) + } else { + deploymentManifest.SetLabels(labels) + } + + // Check to see whether deployed resource already exists. If not, create else update + instanceFound := false + item, err := a.Client.DynamicClient.Resource(gvr).Namespace(namespace).Get(deploymentManifest.GetName(), metav1.GetOptions{}) + if item != nil && err == nil { + instanceFound = true + deploymentManifest.SetResourceVersion(item.GetResourceVersion()) + deploymentManifest.SetAnnotations(item.GetAnnotations()) + // If deployment is a `Service` of type `ClusterIP` then the service in the manifest will probably not + // have a ClusterIP defined, as this is determined when the manifest is applied. When updating the Service + // the manifest cannot have an empty `ClusterIP` defintion, so we need to copy this from the existing definition. + if item.GetKind() == "Service" { + currentServiceSpec := item.UnstructuredContent()["spec"].(map[string]interface{}) + if currentServiceSpec["clusterIP"] != nil && currentServiceSpec["clusterIP"] != "" { + newService := deploymentManifest.UnstructuredContent() + newService["spec"].(map[string]interface{})["clusterIP"] = currentServiceSpec["clusterIP"] + deploymentManifest.SetUnstructuredContent(newService) + } + } + } + + actionType := "Creating" + if instanceFound { + actionType = "Updating" // Update deployment + } + s := log.Spinnerf("%s resource of kind %s", strings.Title(actionType), gvk.Kind) + var result *unstructured.Unstructured + if !instanceFound { + result, err = a.Client.DynamicClient.Resource(gvr).Namespace(namespace).Create(deploymentManifest, metav1.CreateOptions{}) + } else { + result, err = a.Client.DynamicClient.Resource(gvr).Namespace(namespace).Update(deploymentManifest, metav1.UpdateOptions{}) + } + if err != nil { + s.End(false) + return errors.Wrapf(err, "Failed when %s manifest %s", actionType, gvk.Kind) + } + s.End(true) + + if imageStream != nil { + ownerReference := metav1.OwnerReference{ + APIVersion: result.GetAPIVersion(), + Kind: result.GetKind(), + Name: result.GetName(), + UID: result.GetUID(), + } + + imageStream.ObjectMeta.OwnerReferences = append(imageStream.ObjectMeta.OwnerReferences, ownerReference) + } + + // Write the returned manifest to the local manifest file + if writtenToManifest { + _, err = manifestFile.WriteString("---\n") + if err != nil { + return errors.Wrap(err, "Unable to write to local manifest file") + } + } + err = yamlDecoder.Encode(result, manifestFile) + if err != nil { + return errors.Wrap(err, "Unable to write to local manifest file") + } + writtenToManifest = true + } + } + + if imageStream != nil { + err = client.UpdateImageStream(imageStream) + if err != nil { + return err + } + } + s := log.Spinner("Determining the application URL") + + // Need to wait for a second to give the server time to create the artifacts + // TODO: Replace wait with a wait for object to be created correctly + time.Sleep(2 * time.Second) + + labelSelector := fmt.Sprintf("%v=%v", "component", applicationName) + fullURL, err := client.GetApplicationURL(applicationName, labelSelector) + if err != nil { + s.End(false) + log.Warningf("Unable to determine the application URL for component %s: %s", a.ComponentName, err) + } else { + s.End(true) + log.Successf("Successfully deployed component: %s", fullURL) + } + + return nil +} + +func (a Adapter) DeployDelete(manifest []byte) (err error) { + deploymentManifest := &unstructured.Unstructured{} + // Build a yaml decoder with the unstructured Scheme + yamlDecoder := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + manifests := bytes.Split(manifest, []byte("---")) + for _, splitManifest := range manifests { + if len(manifest) > 0 { + _, gvk, err := yamlDecoder.Decode([]byte(splitManifest), nil, deploymentManifest) + if err != nil { + return err + } + klog.V(3).Infof("Deploy manifest:\n\n%s", deploymentManifest) + kind := utils.PluraliseKind(gvk.Kind) + gvr := schema.GroupVersionResource{Group: gvk.Group, Version: gvk.Version, Resource: kind} + klog.V(3).Infof("Manifest type: %s", gvr.String()) + + _, err = a.Client.DynamicClient.Resource(gvr).Namespace(a.Client.Namespace).Get(deploymentManifest.GetName(), metav1.GetOptions{}) + if err != nil { + errorMessage := "Could not delete component " + deploymentManifest.GetName() + " as component was not found" + return errors.New(errorMessage) + } + + err = a.Client.DynamicClient.Resource(gvr).Namespace(a.Client.Namespace).Delete(deploymentManifest.GetName(), &metav1.DeleteOptions{}) + if err != nil { + return err + } + } + } + return nil +} + // Push updates the component if a matching component exists or creates one if it doesn't exist // Once the component has started, it will sync the source code to it. func (a Adapter) Push(parameters common.PushParameters) (err error) { diff --git a/pkg/devfile/adapters/kubernetes/utils/utils.go b/pkg/devfile/adapters/kubernetes/utils/utils.go index b7565601587..9e9d82fd99b 100644 --- a/pkg/devfile/adapters/kubernetes/utils/utils.go +++ b/pkg/devfile/adapters/kubernetes/utils/utils.go @@ -308,6 +308,18 @@ func overrideContainerArgs(container *corev1.Container) { container.Args = append(container.Args, "-c", adaptersCommon.SupervisordConfFile) } +func PluraliseKind(gvkKind string) (kind string) { + // gvkKind is normally a singular noun and we need to have the kind as a plural + // i.e. Deploment => Deployments + // Route => Routes + // Ingress => Ingresses + kind = strings.ToLower(gvkKind + "s") + if strings.HasSuffix(gvkKind, "s") { + kind = strings.ToLower(gvkKind + "es") + } + return kind +} + // UpdateContainerWithEnvFrom populates the runtime container with relevant // values for "EnvFrom" so that component can be linked with Operator backed // service diff --git a/pkg/devfile/parser/data/2.0.0/devfileJsonSchema200.go b/pkg/devfile/parser/data/2.0.0/devfileJsonSchema200.go index 66c2e1b71a4..22efe9577db 100644 --- a/pkg/devfile/parser/data/2.0.0/devfileJsonSchema200.go +++ b/pkg/devfile/parser/data/2.0.0/devfileJsonSchema200.go @@ -3854,7 +3854,15 @@ const JsonSchema200 = `{ "name": { "type": "string", "description": "Optional devfile name" - } + }, + "alpha.build-dockerfile": { + "type":"string", + "description": "Optional URL to remote Dockerfile" + }, + "alpha.deployment-manifest": { + "type":"string", + "description": "Optional URL to remote Deployment Manifest" + } } }, "schemaVersion": { diff --git a/pkg/devfile/parser/data/common/types.go b/pkg/devfile/parser/data/common/types.go index 9631acc180b..a70fab90356 100644 --- a/pkg/devfile/parser/data/common/types.go +++ b/pkg/devfile/parser/data/common/types.go @@ -55,7 +55,13 @@ type DevfileMetadata struct { Name string `json:"name,omitempty" yaml:"name,omitempty"` // Version Optional semver-compatible version - Version string `json:"version,omitempty" yaml:"version,omitempty"` + Version string `json:"version,omitempty"` + + // Dockerfile optional URL to remote Dockerfile + Dockerfile string `json:"alpha.build-dockerfile,omitempty"` + + // Manifest optional URL to remote Deployment Manifest + Manifest string `json:"alpha.deployment-manifest,omitempty"` } // DevfileCommand command specified in devfile diff --git a/pkg/envinfo/envinfo.go b/pkg/envinfo/envinfo.go index 102d3ac0225..5127ce76f9c 100644 --- a/pkg/envinfo/envinfo.go +++ b/pkg/envinfo/envinfo.go @@ -1,6 +1,7 @@ package envinfo import ( + "fmt" "io" "os" "path/filepath" @@ -397,6 +398,16 @@ func (ei *EnvInfo) GetDebugPort() int { return *ei.componentSettings.DebugPort } +// GetPortByURLKind returns the Port of a specific URL type, returns 0 if nil +func (ei *EnvInfo) GetPortByURLKind(urlKind URLKind) (int, error) { + for _, localURL := range ei.GetURL() { + if localURL.Kind == urlKind { + return localURL.Port, nil + } + } + return 0, errors.New(fmt.Sprintf("unable to find port for URL of kind: '%s'", urlKind)) +} + // GetRunMode returns the RunMode, returns default if nil func (ei *EnvInfo) GetRunMode() RUNMode { if ei.componentSettings.RunMode == nil { diff --git a/pkg/occlient/occlient.go b/pkg/occlient/occlient.go index ee7216a41e4..0f81aac07ed 100644 --- a/pkg/occlient/occlient.go +++ b/pkg/occlient/occlient.go @@ -56,7 +56,10 @@ import ( "k8s.io/apimachinery/pkg/version" "k8s.io/apimachinery/pkg/watch" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" @@ -147,6 +150,8 @@ const ( EnvS2IWorkingDir = "ODO_S2I_WORKING_DIR" DefaultAppRootDir = "/opt/app-root" + + DeployWaitTimeout = 30 * time.Second ) // S2IPaths is a struct that will hold path to S2I scripts and the protocol indicating access to them, component source/binary paths, artifacts deployments directory @@ -210,6 +215,7 @@ type Client struct { KubeConfig clientcmd.ClientConfig discoveryClient discovery.DiscoveryInterface Namespace string + dynamicClient dynamic.Interface } func (c *Client) SetDiscoveryInterface(client discovery.DiscoveryInterface) { @@ -291,6 +297,11 @@ func New() (*Client, error) { } client.Namespace = namespace + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + return nil, err + } + client.dynamicClient = dynamicClient return &client, nil } @@ -394,6 +405,31 @@ func (c *Client) GetPortsFromBuilderImage(componentType string) ([]string, error return portList, nil } +// RunBuildConfigWithBinary +func (c *Client) RunBuildConfigWithBinaryInput(name string, r io.Reader) (*buildv1.Build, error) { + // TODO: investigate this issue + // Error: no kind is registered for the type v1.BinaryBuildRequestOptions in scheme "k8s.io/client-go/kubernetes/scheme/register.go:69" + // options := &buildv1.BinaryBuildRequestOptions{ + // ObjectMeta: metav1.ObjectMeta{ + // Name: name, + // Namespace: c.Namespace, + // }, + // } + result := &buildv1.Build{} + err := c.buildClient.RESTClient(). + Post(). + Namespace(c.Namespace). + Resource("buildconfigs"). + Name(name). + SubResource("instantiatebinary"). + Body(r). + //VersionedParams(options, scheme.ParameterCodec). + Do(). + Into(result) + + return result, err +} + // RunLogout logs out the current user from cluster func (c *Client) RunLogout(stdout io.Writer) error { output, err := c.userClient.Users().Get("~", metav1.GetOptions{}) @@ -678,6 +714,7 @@ func isTagInImageStream(is imagev1.ImageStream, imageTag string) bool { // imagestream of required tag not found in current namespace, then searches openshift namespace. // If not found, error out. If imageNS is not empty string, then, the requested imageNS only is searched // for requested imagestream +// if imageTag is empty string, then do not check for tag in the imagestream. func (c *Client) GetImageStream(imageNS string, imageName string, imageTag string) (*imagev1.ImageStream, error) { var err error var imageStream *imagev1.ImageStream @@ -701,7 +738,7 @@ func (c *Client) GetImageStream(imageNS string, imageName string, imageTag strin currentNSImageStream, e := c.imageClient.ImageStreams(currentProjectName).Get(imageName, metav1.GetOptions{}) if e != nil { err = errors.Wrapf(e, "no match found for : %s in namespace %s", imageName, currentProjectName) - } else { + } else if imageTag != "" { if isTagInImageStream(*currentNSImageStream, imageTag) { return currentNSImageStream, nil } @@ -712,7 +749,7 @@ func (c *Client) GetImageStream(imageNS string, imageName string, imageTag strin if e != nil { // The image is not available in current Namespace. err = errors.Wrapf(e, "no match found for : %s in namespace %s", imageName, OpenShiftNameSpace) - } else { + } else if imageTag != "" { if isTagInImageStream(*openshiftNSImageStream, imageTag) { return openshiftNSImageStream, nil } @@ -733,8 +770,11 @@ func (c *Client) GetImageStream(imageNS string, imageName string, imageTag strin err, "no match found for %s in namespace %s", imageName, imageNS, ) } - if !isTagInImageStream(*imageStream, imageTag) { - return nil, fmt.Errorf("image stream %s with tag %s not found in %s namespaces", imageName, imageTag, currentProjectName) + + if imageTag != "" { + if !isTagInImageStream(*imageStream, imageTag) { + return nil, fmt.Errorf("image stream %s with tag %s not found in %s namespaces", imageName, imageTag, currentProjectName) + } } return imageStream, nil @@ -2759,6 +2799,11 @@ func (c *Client) GetDeploymentConfigsFromSelector(selector string) ([]appsv1.Dep return dcList.Items, nil } +func (c *Client) GetServiceFromName(name string) (*corev1.Service, error) { + service, err := c.kubeClient.CoreV1().Services(c.Namespace).Get(name, metav1.GetOptions{}) + return service, err +} + // GetServicesFromSelector returns an array of Service resources which match the // given selector func (c *Client) GetServicesFromSelector(selector string) ([]corev1.Service, error) { @@ -3064,6 +3109,37 @@ func (c *Client) GetPVCFromName(pvcName string) (*corev1.PersistentVolumeClaim, return c.kubeClient.CoreV1().PersistentVolumeClaims(c.Namespace).Get(pvcName, metav1.GetOptions{}) } +// CreateDockerBuildConfigWithBinaryAndDockerfile creates a BuildConfig which accepts +// as input a binary which contains a dockerfile at a specific location. It will build +// the source with Dockerfile, and push the image using tag. +// envVars is the array containing the environment variables +func (c *Client) CreateDockerBuildConfigWithBinaryInput(commonObjectMeta metav1.ObjectMeta, dockerfilePath string, outputImageTag string, envVars []corev1.EnvVar, outputType string) (bc buildv1.BuildConfig, err error) { + // generate and create ImageStream if not present + + var imageStream *imagev1.ImageStream + if imageStream, err = c.GetImageStream(c.Namespace, commonObjectMeta.Name, ""); err != nil || imageStream == nil { + imageStream = &imagev1.ImageStream{ + ObjectMeta: commonObjectMeta, + } + + _, err = c.imageClient.ImageStreams(c.Namespace).Create(imageStream) + if err != nil { + return bc, errors.Wrapf(err, "unable to create ImageStream for %s", commonObjectMeta.Name) + } + } + + bc = generateDockerBuildConfigWithBinaryInput(commonObjectMeta, dockerfilePath, outputImageTag, outputType) + + if len(envVars) > 0 { + bc.Spec.Strategy.SourceStrategy.Env = envVars + } + _, err = c.buildClient.BuildConfigs(c.Namespace).Create(&bc) + if err != nil { + return bc, errors.Wrapf(err, "unable to create BuildConfig for %s", commonObjectMeta.Name) + } + return bc, err +} + // CreateBuildConfig creates a buildConfig using the builderImage as well as gitURL. // envVars is the array containing the environment variables func (c *Client) CreateBuildConfig(commonObjectMeta metav1.ObjectMeta, builderImage string, gitURL string, gitRef string, envVars []corev1.EnvVar) (buildv1.BuildConfig, error) { @@ -3258,6 +3334,12 @@ func isSubDir(baseDir, otherDir string) bool { return matches } +// IsBuildConfigSupported checks if buildconfig resource type is present on the cluster +func (c *Client) IsBuildConfigSupported() (bool, error) { + + return c.isResourceSupported("build.openshift.io", "v1", "buildconfigs") +} + // IsRouteSupported checks if route resource type is present on the cluster func (c *Client) IsRouteSupported() (bool, error) { @@ -3314,3 +3396,105 @@ func (c *Client) isResourceSupported(apiGroup, apiVersion, resourceName string) } return false, nil } + +// UpdateImageStreamOwnerReference +func (c *Client) UpdateImageStream(imageStream *imagev1.ImageStream) (err error) { + _, err = c.imageClient.ImageStreams(c.Namespace).Update(imageStream) + return +} + +func (c *Client) GetApplicationURLFromService(applicationName string) (fullURL string) { + service, err := c.GetServiceFromName(applicationName) + if err != nil { + return "" + } + + if service.Status.LoadBalancer.Size() > 0 { + fullURL = fmt.Sprintf("http://%s:%d", service.Status.LoadBalancer.Ingress[0].Hostname, service.Spec.Ports[0].NodePort) + } + + return fullURL +} + +func (c *Client) GetApplicationURL(applicationName, labelSelector string) (fullURL string, err error) { + routeSupported, _ := c.IsRouteSupported() + if routeSupported { + routes, err := c.ListRoutes(labelSelector) + if err != nil || len(routes) <= 0 { + // No URL found - try looking in the Service + fullURL = c.GetApplicationURLFromService(applicationName) + if fullURL == "" { + // still no URL found - try looking for a knative Route therefore need to wait for Service and Route to be setup. + knGvr := schema.GroupVersionResource{Group: "serving.knative.dev", Version: "v1", Resource: "routes"} + knRoute, err := c.waitForManifestDeployCompletion(applicationName, knGvr, "Ready") + if err != nil { + return "", errors.Wrap(err, "error while waiting for deployment completion") + } + fullURL = knRoute.UnstructuredContent()["status"].(map[string]interface{})["url"].(string) + } + } else { + if len(routes) == 1 { + fullURL = fmt.Sprintf("%s://%s", getRouteProtocol(routes[0]), routes[0].Spec.Host) + } else { + return "", errors.New("multiple routes found") + } + } + } else { + // TODO: Look for other resources, ie Ingress + return "", errors.New("Route not supported") + } + + if fullURL == "" { + return "", errors.New("URL not able to be found") + } + + return fullURL, nil +} + +// Create a function to wait for deploment completion of any unstructured object +func (c *Client) waitForManifestDeployCompletion(applicationName string, gvr schema.GroupVersionResource, conditionTypeValue string) (*unstructured.Unstructured, error) { + klog.V(4).Infof("Waiting for %s manifest deployment completion", applicationName) + w, err := c.dynamicClient.Resource(gvr).Namespace(c.Namespace).Watch(metav1.ListOptions{FieldSelector: "metadata.name=" + applicationName}) + if err != nil { + return nil, errors.Wrapf(err, "unable to watch deployment") + } + defer w.Stop() + success := make(chan *unstructured.Unstructured) + failure := make(chan error) + + go func() { + defer close(success) + defer close(failure) + + for { + val, ok := <-w.ResultChan() + if !ok { + failure <- errors.New("watch channel was closed") + return + } + if watchObject, ok := val.Object.(*unstructured.Unstructured); ok { + // TODO: Add more details on what to check to see if object deployment is complete + // Currently only checks to see if status.conditions[] contains a condition with type = conditionTypeValue + condition := getNamedConditionFromObjectStatus(watchObject, conditionTypeValue) + if condition != nil { + if condition["status"] == "Fail" { + failure <- fmt.Errorf("manifest deployment %s failed", applicationName) + return + } else if condition["status"] == "True" { + success <- watchObject + return + } + } + } + } + }() + + select { + case val := <-success: + return val, nil + case err := <-failure: + return nil, err + case <-time.After(DeployWaitTimeout): + return nil, errors.Errorf("timeout while waiting for %s manifest deployment completion", applicationName) + } +} diff --git a/pkg/occlient/templates.go b/pkg/occlient/templates.go index 2fa6e4c6225..a59c0193dcc 100644 --- a/pkg/occlient/templates.go +++ b/pkg/occlient/templates.go @@ -315,6 +315,39 @@ func generateGitDeploymentConfig(commonObjectMeta metav1.ObjectMeta, image strin return dc } +// generateDockerBuildConfigWithBinaryInput creates a BuildConfig which accepts a Binary (usualy archive) +// with a Dockerfile at the path specified. It will run a build using docker, and push the resulting image +// to the tag specified. +// outputType can be either DockerImage or ImageStreamTag +func generateDockerBuildConfigWithBinaryInput(commonObjectMeta metav1.ObjectMeta, dockerfilePath, outputImageTag, outputType string) buildv1.BuildConfig { + buildSource := buildv1.BuildSource{ + Binary: &buildv1.BinaryBuildSource{}, + Type: buildv1.BuildSourceBinary, + } + + return buildv1.BuildConfig{ + ObjectMeta: commonObjectMeta, + Spec: buildv1.BuildConfigSpec{ + CommonSpec: buildv1.CommonSpec{ + Output: buildv1.BuildOutput{ + To: &corev1.ObjectReference{ + Kind: outputType, + Name: outputImageTag, + }, + // OPTIONAL: PushSecrets and ImageLabels + }, + Source: buildSource, + Strategy: buildv1.BuildStrategy{ + Type: buildv1.DockerBuildStrategyType, + DockerStrategy: &buildv1.DockerBuildStrategy{ + DockerfilePath: dockerfilePath, + }, + }, + }, + }, + } +} + // generateBuildConfig creates a BuildConfig for Git URL's being passed into Odo func generateBuildConfig(commonObjectMeta metav1.ObjectMeta, gitURL, gitRef, imageName, imageNamespace string) buildv1.BuildConfig { diff --git a/pkg/occlient/utils.go b/pkg/occlient/utils.go index 9c6fcca420e..35946e8148d 100644 --- a/pkg/occlient/utils.go +++ b/pkg/occlient/utils.go @@ -5,9 +5,11 @@ import ( appsv1 "github.com/openshift/api/apps/v1" imagev1 "github.com/openshift/api/image/v1" + routev1 "github.com/openshift/api/route/v1" "github.com/openshift/library-go/pkg/apps/appsutil" "github.com/openshift/odo/pkg/config" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/klog" ) @@ -89,6 +91,29 @@ func IsDCRolledOut(config *appsv1.DeploymentConfig, desiredRevision int64) bool return false } +// GetProtocol returns the protocol string +func getRouteProtocol(route routev1.Route) string { + if route.Spec.TLS != nil { + return "https" + } + return "http" +} + +func getNamedConditionFromObjectStatus(baseObject *unstructured.Unstructured, conditionTypeValue string) map[string]interface{} { + status := baseObject.UnstructuredContent()["status"].(map[string]interface{}) + if status != nil && status["conditions"] != nil { + conditions := status["conditions"].([]interface{}) + for i := range conditions { + c := conditions[i].(map[string]interface{}) + klog.V(4).Infof("Condition returned\n%s\n", c) + if c["type"] == conditionTypeValue { + return c + } + } + } + return nil +} + // GetS2IEnvForDevfile gets environment variable for builder image to be added in devfiles func GetS2IEnvForDevfile(sourceType string, env config.EnvVarList, imageStreamImage imagev1.ImageStreamImage) (config.EnvVarList, error) { klog.V(2).Info("Get S2I environment variables to be added in devfile") diff --git a/pkg/odo/cli/cli.go b/pkg/odo/cli/cli.go index 536dea272d7..04a31ca9995 100644 --- a/pkg/odo/cli/cli.go +++ b/pkg/odo/cli/cli.go @@ -25,6 +25,7 @@ import ( "github.com/openshift/odo/pkg/odo/cli/version" "github.com/openshift/odo/pkg/odo/util" odoutil "github.com/openshift/odo/pkg/odo/util" + "github.com/openshift/odo/pkg/odo/util/experimental" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -207,6 +208,11 @@ func odoRootCmd(name, fullName string) *cobra.Command { component.NewCmdTest(component.TestRecommendedCommandName, util.GetFullName(fullName, component.TestRecommendedCommandName)), env.NewCmdEnv(env.RecommendedCommandName, util.GetFullName(fullName, env.RecommendedCommandName)), ) + if experimental.IsExperimentalModeEnabled() { + rootCmd.AddCommand( + component.NewCmdDeploy(component.DeployRecommendedCommandName, util.GetFullName(fullName, component.DeployRecommendedCommandName)), + ) + } odoutil.VisitCommands(rootCmd, reconfigureCmdWithSubcmd) @@ -224,6 +230,7 @@ func reconfigureCmdWithSubcmd(cmd *cobra.Command) { if cmd.Args == nil { cmd.Args = cobra.ArbitraryArgs } + if cmd.RunE == nil { cmd.RunE = ShowSubcommands } diff --git a/pkg/odo/cli/component/component.go b/pkg/odo/cli/component/component.go index 7443ece2592..709285ec878 100644 --- a/pkg/odo/cli/component/component.go +++ b/pkg/odo/cli/component/component.go @@ -9,6 +9,7 @@ import ( "github.com/openshift/odo/pkg/log" "github.com/openshift/odo/pkg/occlient" "github.com/openshift/odo/pkg/odo/util/completion" + "github.com/openshift/odo/pkg/odo/util/experimental" "github.com/openshift/odo/pkg/url" "github.com/pkg/errors" @@ -47,6 +48,7 @@ func NewCmdComponent(name, fullName string) *cobra.Command { createCmd := NewCmdCreate(CreateRecommendedCommandName, odoutil.GetFullName(fullName, CreateRecommendedCommandName)) deleteCmd := NewCmdDelete(DeleteRecommendedCommandName, odoutil.GetFullName(fullName, DeleteRecommendedCommandName)) describeCmd := NewCmdDescribe(DescribeRecommendedCommandName, odoutil.GetFullName(fullName, DescribeRecommendedCommandName)) + deployCmd := NewCmdDeploy(DeployRecommendedCommandName, odoutil.GetFullName(fullName, DeployRecommendedCommandName)) linkCmd := NewCmdLink(LinkRecommendedCommandName, odoutil.GetFullName(fullName, LinkRecommendedCommandName)) unlinkCmd := NewCmdUnlink(UnlinkRecommendedCommandName, odoutil.GetFullName(fullName, UnlinkRecommendedCommandName)) listCmd := NewCmdList(ListRecommendedCommandName, odoutil.GetFullName(fullName, ListRecommendedCommandName)) @@ -74,6 +76,10 @@ func NewCmdComponent(name, fullName string) *cobra.Command { componentCmd.AddCommand(componentGetCmd, createCmd, deleteCmd, describeCmd, linkCmd, unlinkCmd, listCmd, logCmd, pushCmd, updateCmd, watchCmd, execCmd) componentCmd.AddCommand(testCmd) + if experimental.IsExperimentalModeEnabled() { + componentCmd.AddCommand(deployCmd) + } + // Add a defined annotation in order to appear in the help menu componentCmd.Annotations = map[string]string{"command": "main"} componentCmd.SetUsageTemplate(odoutil.CmdUsageTemplate) diff --git a/pkg/odo/cli/component/deploy.go b/pkg/odo/cli/component/deploy.go new file mode 100644 index 00000000000..b17deed4279 --- /dev/null +++ b/pkg/odo/cli/component/deploy.go @@ -0,0 +1,221 @@ +package component + +import ( + "bytes" + "fmt" + "path/filepath" + + devfile "github.com/openshift/odo/pkg/devfile" + devfileParser "github.com/openshift/odo/pkg/devfile/parser" + "github.com/openshift/odo/pkg/envinfo" + "github.com/openshift/odo/pkg/log" + projectCmd "github.com/openshift/odo/pkg/odo/cli/project" + "github.com/openshift/odo/pkg/odo/genericclioptions" + "github.com/openshift/odo/pkg/odo/util/completion" + "github.com/openshift/odo/pkg/util" + "github.com/pkg/errors" + "github.com/spf13/cobra" + + odoutil "github.com/openshift/odo/pkg/odo/util" + + ktemplates "k8s.io/kubectl/pkg/util/templates" +) + +var deployCmdExample = ktemplates.Examples(` # Build and Deploy the current component +%[1]s + +# Specify the tag for the image by calling +%[1]s --tag //: + `) + +// DeployRecommendedCommandName is the recommended build command name +const DeployRecommendedCommandName = "deploy" + +// DeployOptions encapsulates options that build command uses +type DeployOptions struct { + componentContext string + sourcePath string + ignores []string + EnvSpecificInfo *envinfo.EnvSpecificInfo + + DevfilePath string + devObj devfileParser.DevfileObj + DockerfileURL string + DockerfileBytes []byte + namespace string + tag string + ManifestSource []byte + DeploymentPort int + + *genericclioptions.Context +} + +// NewDeployOptions returns new instance of BuildOptions +// with "default" values for certain values, for example, show is "false" +func NewDeployOptions() *DeployOptions { + return &DeployOptions{} +} + +// CompleteDevfilePath completes the devfile path from context +func (do *DeployOptions) CompleteDevfilePath() { + if len(do.DevfilePath) > 0 { + do.DevfilePath = filepath.Join(do.componentContext, do.DevfilePath) + } else { + do.DevfilePath = filepath.Join(do.componentContext, "devfile.yaml") + } +} + +// Complete completes deploy args +func (do *DeployOptions) Complete(name string, cmd *cobra.Command, args []string) (err error) { + do.CompleteDevfilePath() + envInfo, err := envinfo.NewEnvSpecificInfo(do.componentContext) + if err != nil { + return errors.Wrap(err, "unable to retrieve configuration information") + } + do.EnvSpecificInfo = envInfo + do.Context = genericclioptions.NewDevfileContext(cmd) + + return nil +} + +// Validate validates the push parameters +func (do *DeployOptions) Validate() (err error) { + + log.Infof("\nValidation") + + // Validate the --tag + s := log.Spinner("Validating arguments") + + // Empty tag will be set in the adapter + // based on the namespace and component name + if do.tag != "" { + err = util.ValidateTag(do.tag) + if err != nil { + s.End(false) + return err + } + } + s.End(true) + + do.devObj, err = devfile.ParseAndValidate(do.DevfilePath) + if err != nil { + return err + } + + s = log.Spinner("Validating build information") + metadata := do.devObj.Data.GetMetadata() + dockerfileURL := metadata.Dockerfile + + //Download Dockerfile to .odo, build, then delete from .odo dir + //If Dockerfile is present in the project already, use that for the build + //If Dockerfile is present in the project and field is in devfile, build the one already in the project and warn the user. + if dockerfileURL != "" && util.CheckPathExists(filepath.Join(do.componentContext, "Dockerfile")) { + // TODO: make clearer more visible output + log.Warning("Dockerfile already exists in project directory and one is specified in Devfile.") + log.Warningf("Using Dockerfile specified in devfile from '%s'", dockerfileURL) + } + + if dockerfileURL != "" { + dockerfileBytes, err := util.LoadFileIntoMemory(dockerfileURL) + if err != nil { + s.End(false) + return errors.New("unable to download Dockerfile from URL specified in devfile") + } + // If we successfully downloaded the Dockerfile into memory, store it in the DeployOptions + do.DockerfileBytes = dockerfileBytes + + // Validate the file that was downloaded is a Dockerfile + err = util.ValidateDockerfile(dockerfileBytes) + if err != nil { + s.End(false) + return err + } + + } else if !util.CheckPathExists(filepath.Join(do.componentContext, "Dockerfile")) { + s.End(false) + return errors.New("dockerfile required for build. No 'alpha.build-dockerfile' field found in devfile, or Dockerfile found in project directory") + } + + s.End(true) + + s = log.Spinner("Validating deployment information") + manifestURL := metadata.Manifest + + if manifestURL == "" { + s.End(false) + return errors.New("Unable to deploy as alpha.deployment-manifest is not defined in devfile.yaml") + } + + manifestBytes, err := util.LoadFileIntoMemory(manifestURL) + if err != nil { + s.End(false) + return errors.Wrap(err, "unable to download manifest from URL specified in devfile") + } + do.ManifestSource = manifestBytes + + // check if manifestSource contains {{.PORT}} template variable + // if it does, then check we have an port setup in env.yaml + do.DeploymentPort = 0 + if bytes.Contains(manifestBytes, []byte("{{.PORT}}")) { + deploymentPort, err := do.EnvSpecificInfo.GetPortByURLKind(envinfo.ROUTE) + if err != nil { + s.End(false) + return errors.Wrap(err, "unable to find `port` for deployment. `odo url create` must be run prior to `odo deploy`") + } + do.DeploymentPort = deploymentPort + } + + s.End(true) + + return +} + +// Run has the logic to perform the required actions as part of command +func (do *DeployOptions) Run() (err error) { + err = do.DevfileDeploy() + if err != nil { + return err + } + + return nil +} + +// Need to use RunE on Cobra command to allow for `odo deploy` and `odo deploy delete` +// See reconfigureCmdWithSubCmd function in cli.go +func (do *DeployOptions) deployRunE(cmd *cobra.Command, args []string) error { + genericclioptions.GenericRun(do, cmd, args) + return nil +} + +// NewCmdDeploy implements the push odo command +func NewCmdDeploy(name, fullName string) *cobra.Command { + do := NewDeployOptions() + + deployDeleteCmd := NewCmdDeployDelete(DeployDeleteRecommendedCommandName, odoutil.GetFullName(fullName, DeployDeleteRecommendedCommandName)) + + var deployCmd = &cobra.Command{ + Use: fmt.Sprintf("%s [command] [component name]", name), + Short: "Build and deploy image for component", + Long: `Build and deploy image for component`, + Example: fmt.Sprintf(deployCmdExample, fullName), + Args: cobra.MaximumNArgs(1), + Annotations: map[string]string{"command": "component"}, + RunE: do.deployRunE, + } + genericclioptions.AddContextFlag(deployCmd, &do.componentContext) + + // enable devfile flag if experimental mode is enabled + deployCmd.Flags().StringVar(&do.tag, "tag", "", "Tag used to build the image. In the format /namespace>/") + + deployCmd.Flags().StringSliceVar(&do.ignores, "ignore", []string{}, "Files or folders to be ignored via glob expressions.") + + //Adding `--project` flag + projectCmd.AddProjectFlag(deployCmd) + + deployCmd.AddCommand(deployDeleteCmd) + deployCmd.SetUsageTemplate(odoutil.CmdUsageTemplate) + completion.RegisterCommandHandler(deployCmd, completion.ComponentNameCompletionHandler) + completion.RegisterCommandFlagHandler(deployCmd, "context", completion.FileCompletionHandler) + + return deployCmd +} diff --git a/pkg/odo/cli/component/deploy_delete.go b/pkg/odo/cli/component/deploy_delete.go new file mode 100644 index 00000000000..edf93d8e5e3 --- /dev/null +++ b/pkg/odo/cli/component/deploy_delete.go @@ -0,0 +1,124 @@ +package component + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/openshift/odo/pkg/envinfo" + projectCmd "github.com/openshift/odo/pkg/odo/cli/project" + "github.com/openshift/odo/pkg/odo/genericclioptions" + "github.com/openshift/odo/pkg/odo/util/completion" + "github.com/pkg/errors" + "github.com/spf13/cobra" + + odoutil "github.com/openshift/odo/pkg/odo/util" + + ktemplates "k8s.io/kubectl/pkg/util/templates" +) + +// Constant for manifest +const manifestFile = ".odo/manifest.yaml" + +// TODO: add CLI Reference doc +var deployDeleteCmdExample = ktemplates.Examples(` # Delete deployed component +%[1]s + `) + +// DeployDeleteRecommendedCommandName is the recommended build command name +const DeployDeleteRecommendedCommandName = "delete" + +// DeployDeleteOptions encapsulates options that deploy delete command uses +type DeployDeleteOptions struct { + componentContext string + EnvSpecificInfo *envinfo.EnvSpecificInfo + + DevfilePath string + namespace string + ManifestPath string + ManifestSource []byte + + *genericclioptions.Context +} + +// NewDeployDeleteOptions returns new instance of DeployDeleteOptions +// with "default" values for certain values, for example, show is "false" +func NewDeployDeleteOptions() *DeployDeleteOptions { + return &DeployDeleteOptions{} +} + +// CompleteDevfilePath completes the devfile path from context +func (ddo *DeployDeleteOptions) CompleteDevfilePath() { + if len(ddo.DevfilePath) > 0 { + ddo.DevfilePath = filepath.Join(ddo.componentContext, ddo.DevfilePath) + } else { + ddo.DevfilePath = filepath.Join(ddo.componentContext, "devfile.yaml") + } +} + +// Complete completes push args +func (ddo *DeployDeleteOptions) Complete(name string, cmd *cobra.Command, args []string) (err error) { + ddo.CompleteDevfilePath() + envInfo, err := envinfo.NewEnvSpecificInfo(ddo.componentContext) + if err != nil { + return errors.Wrap(err, "unable to retrieve configuration information") + } + ddo.EnvSpecificInfo = envInfo + ddo.Context = genericclioptions.NewDevfileContext(cmd) + + return nil +} + +// Validate validates the push parameters +func (ddo *DeployDeleteOptions) Validate() (err error) { + // ddo.componentContext, .odo, manifest.yaml + // TODO: Check manifest is actually there!!! + // read bytes into deployDeleteOptions + if _, err := os.Stat(manifestFile); os.IsNotExist(err) { + return errors.Wrap(err, "manifest file at "+manifestFile+" does not exist") + } + + ddo.ManifestSource, err = ioutil.ReadFile(manifestFile) + if err != nil { + return err + } + ddo.ManifestPath = manifestFile + return +} + +// Run has the logic to perform the required actions as part of command +func (ddo *DeployDeleteOptions) Run() (err error) { + err = ddo.DevfileDeployDelete() + if err != nil { + return err + } + + return nil +} + +// NewCmdDeploy implements the push odo command +func NewCmdDeployDelete(name, fullName string) *cobra.Command { + ddo := NewDeployDeleteOptions() + + var deployDeleteCmd = &cobra.Command{ + Use: name, + Short: "Delete deployed component", + Long: "Delete deployed component", + Example: fmt.Sprintf(deployDeleteCmdExample, fullName), + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + genericclioptions.GenericRun(ddo, cmd, args) + }, + } + genericclioptions.AddContextFlag(deployDeleteCmd, &ddo.componentContext) + + //Adding `--project` flag + projectCmd.AddProjectFlag(deployDeleteCmd) + + deployDeleteCmd.SetUsageTemplate(odoutil.CmdUsageTemplate) + completion.RegisterCommandHandler(deployDeleteCmd, completion.ComponentNameCompletionHandler) + completion.RegisterCommandFlagHandler(deployDeleteCmd, "context", completion.FileCompletionHandler) + + return deployDeleteCmd +} diff --git a/pkg/odo/cli/component/devfile.go b/pkg/odo/cli/component/devfile.go index 790abc907fb..88a5d4d8ea3 100644 --- a/pkg/odo/cli/component/devfile.go +++ b/pkg/odo/cli/component/devfile.go @@ -1,11 +1,13 @@ package component import ( + "fmt" "os" "path/filepath" "strings" "github.com/openshift/odo/pkg/devfile" + "github.com/openshift/odo/pkg/devfile/adapters" "github.com/openshift/odo/pkg/envinfo" "github.com/openshift/odo/pkg/machineoutput" "github.com/openshift/odo/pkg/odo/genericclioptions" @@ -13,7 +15,6 @@ import ( "github.com/openshift/odo/pkg/util" "github.com/pkg/errors" - "github.com/openshift/odo/pkg/devfile/adapters" "github.com/openshift/odo/pkg/devfile/adapters/common" "github.com/openshift/odo/pkg/devfile/adapters/kubernetes" "github.com/openshift/odo/pkg/log" @@ -143,6 +144,122 @@ func (po *PushOptions) devfilePushInner() (err error) { return } +//DevfileDeploy +func (do *DeployOptions) DevfileDeploy() (err error) { + componentName := do.EnvSpecificInfo.GetName() + + // Set the source path to either the context or current working directory (if context not set) + do.sourcePath, err = util.GetAbsPath(do.componentContext) + if err != nil { + return errors.Wrap(err, "unable to get source path") + } + + // Apply ignore information + err = genericclioptions.ApplyIgnore(&do.ignores, do.sourcePath) + if err != nil { + return errors.Wrap(err, "unable to apply ignore information") + } + + kubeContext := kubernetes.KubernetesContext{ + Namespace: do.namespace, + } + + devfileHandler, err := adapters.NewComponentAdapter(componentName, do.componentContext, do.Application, do.devObj, kubeContext) + if err != nil { + return err + } + + buildParams := common.BuildParameters{ + Path: do.sourcePath, + Tag: do.tag, + DockerfileBytes: do.DockerfileBytes, + EnvSpecificInfo: *do.EnvSpecificInfo, + } + + log.Infof("\nBuilding component %s", componentName) + // Build image for the component + err = devfileHandler.Build(buildParams) + if err != nil { + log.Errorf( + "Failed to build component with name %s.\nError: %v", + componentName, + err, + ) + os.Exit(1) + } + + deployParams := common.DeployParameters{ + EnvSpecificInfo: *do.EnvSpecificInfo, + Tag: do.tag, + ManifestSource: do.ManifestSource, + DeploymentPort: do.DeploymentPort, + } + + warnIfURLSInvalid(do.EnvSpecificInfo.GetURL()) + + log.Infof("\nDeploying component %s", componentName) + // Deploy the application + err = devfileHandler.Deploy(deployParams) + if err != nil { + log.Errorf( + "Failed to deploy application with name %s.\nError: %v", + componentName, + err, + ) + os.Exit(1) + } + + return nil +} + +// DevfileComponentDelete deletes the devfile component +func (ddo *DeployDeleteOptions) DevfileDeployDelete() error { + // Parse devfile + devObj, err := devfile.ParseAndValidate(ddo.DevfilePath) + if err != nil { + return err + } + + componentName := ddo.EnvSpecificInfo.GetName() + componentName = componentName + "-deploy" + + kc := kubernetes.KubernetesContext{ + Namespace: ddo.namespace, + } + + devfileHandler, err := adapters.NewComponentAdapter(componentName, ddo.componentContext, ddo.Application, devObj, kc) + if err != nil { + return err + } + + spinner := log.Spinner(fmt.Sprintf("Deleting deployed devfile component %s", componentName)) + defer spinner.End(false) + + manifestErr := devfileHandler.DeployDelete(ddo.ManifestSource) + if manifestErr != nil && strings.Contains(manifestErr.Error(), "as component was not found") { + log.Warning(manifestErr.Error()) + err = os.Remove(ddo.ManifestPath) + if err != nil { + return err + } + spinner.End(false) + log.Success(ddo.ManifestPath + " deleted. Exiting gracefully :)") + return nil + } else if manifestErr != nil { + err = os.Remove(ddo.ManifestPath) + return err + } + + err = os.Remove(ddo.ManifestPath) + if err != nil { + return err + } + + spinner.End(true) + log.Successf("Successfully deleted component") + return nil +} + // DevfileComponentLog fetch and display log from devfile components func (lo LogOptions) DevfileComponentLog() error { // Parse devfile diff --git a/pkg/sync/adapter.go b/pkg/sync/adapter.go index d3a8730b674..d2dde976a65 100644 --- a/pkg/sync/adapter.go +++ b/pkg/sync/adapter.go @@ -2,6 +2,7 @@ package sync import ( "fmt" + "io" "os" "path/filepath" "strings" @@ -29,6 +30,39 @@ type Adapter struct { common.AdapterContext } +// SyncFilesBuild sync the local files to build container volume +func (a Adapter) SyncFilesBuild(buildParameters common.BuildParameters, dockerfilePath string) (reader io.Reader, err error) { + + // If we want to ignore any files + absIgnoreRules := []string{} + if len(buildParameters.IgnoredFiles) > 0 { + absIgnoreRules = util.GetAbsGlobExps(buildParameters.Path, buildParameters.IgnoredFiles) + } + + var s *log.Status + syncFolder := "/" + + s = log.Spinner("Preparing files for building image") + // run the indexer and find the project source files + files, err := util.DeployRunIndexer(buildParameters.Path, absIgnoreRules) + if err != nil { + return reader, err + } + + if len(files) > 0 { + klog.V(4).Infof("Copying files %s to pod", strings.Join(files, " ")) + dockerfile := map[string][]byte{ + dockerfilePath: buildParameters.DockerfileBytes, + } + reader, err = GetTarReader(buildParameters.Path, syncFolder, files, absIgnoreRules, dockerfile) + s.End(true) + return reader, err + } + klog.V(4).Infof("No files to sync") + s.End(true) + return reader, nil +} + // SyncFiles does a couple of things: // if files changed/deleted are passed in from watch, it syncs them to the component // otherwise, it checks which files have changed and syncs the delta diff --git a/pkg/sync/sync.go b/pkg/sync/sync.go index 3d6fc42eac9..6c3324b77ee 100644 --- a/pkg/sync/sync.go +++ b/pkg/sync/sync.go @@ -1,7 +1,7 @@ package sync import ( - taro "archive/tar" + "archive/tar" "io" "io/ioutil" "os" @@ -19,6 +19,53 @@ type SyncClient interface { ExtractProjectToComponent(common.ComponentInfo, string, io.Reader) error } +// GetTarReader creates a tar file from files and return a reader to it +func GetTarReader(localPath string, targetPath string, copyFiles []string, globExps []string, copyBytes map[string][]byte) (reader io.Reader, err error) { + // Destination is set to "ToSlash" as all containers being ran within OpenShift / S2I are all + // Linux based and thus: "\opt\app-root\src" would not work correctly. + dest := filepath.ToSlash(filepath.Join(targetPath, filepath.Base(localPath))) + + klog.V(4).Infof("CopyFile arguments: localPath %s, dest %s, targetPath %s, copyFiles %s, globalExps %s", localPath, dest, targetPath, copyFiles, globExps) + reader, writer := io.Pipe() + // inspired from https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/cp.go#L235 + go func() { + defer writer.Close() + + tarWriter := tar.NewWriter(writer) + + err := makeTar(localPath, dest, copyFiles, globExps, tarWriter) + if err != nil { + log.Errorf("Error while creating tar: %#v", err) + os.Exit(1) + } + + // For each of the files passed, write them to the tar.Writer + // No files can be passed, but if any are, they will be bundled up in the + // tar as well. + for name, content := range copyBytes { + hdr := &tar.Header{ + Name: name, + Mode: 421, + Size: int64(len(content)), + } + err = tarWriter.WriteHeader(hdr) + if err != nil { + log.Errorf("Error writing header for file %s: %#v", name, err) + os.Exit(1) + } + _, err = tarWriter.Write(content) + if err != nil { + log.Errorf("Error writing contents of file %s: %#v", name, err) + os.Exit(1) + } + } + + tarWriter.Close() + }() + + return reader, err +} + // CopyFile copies localPath directory or list of files in copyFiles list to the directory in running Pod. // copyFiles is list of changed files captured during `odo watch` as well as binary file path // During copying binary components, localPath represent base directory path to binary and copyFiles contains path of binary @@ -37,12 +84,15 @@ func CopyFile(client SyncClient, localPath string, compInfo common.ComponentInfo go func() { defer writer.Close() - err := makeTar(localPath, dest, writer, copyFiles, globExps) + tarWriter := tar.NewWriter(writer) + + err := makeTar(localPath, dest, copyFiles, globExps, tarWriter) if err != nil { log.Errorf("Error while creating tar: %#v", err) os.Exit(1) } + tarWriter.Close() }() err := client.ExtractProjectToComponent(compInfo, targetPath, reader) @@ -61,10 +111,9 @@ func checkFileExist(fileName string) bool { // makeTar function is copied from https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/cp.go#L309 // srcPath is ignored if files is set -func makeTar(srcPath, destPath string, writer io.Writer, files []string, globExps []string) error { +func makeTar(srcPath, destPath string, files []string, globExps []string, tarWriter *tar.Writer) error { // TODO: use compression here? - tarWriter := taro.NewWriter(writer) - defer tarWriter.Close() + srcPath = filepath.Clean(srcPath) // "ToSlash" is used as all containers within OpenShift are Linux based @@ -117,7 +166,7 @@ func makeTar(srcPath, destPath string, writer io.Writer, files []string, globExp } // recursiveTar function is copied from https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/cp.go#L319 -func recursiveTar(srcBase, srcFile, destBase, destFile string, tw *taro.Writer, globExps []string) error { +func recursiveTar(srcBase, srcFile, destBase, destFile string, tw *tar.Writer, globExps []string) error { klog.V(4).Infof("recursiveTar arguments: srcBase: %s, srcFile: %s, destBase: %s, destFile: %s", srcBase, srcFile, destBase, destFile) // The destination is a LINUX container and thus we *must* use ToSlash in order @@ -158,7 +207,7 @@ func recursiveTar(srcBase, srcFile, destBase, destFile string, tw *taro.Writer, } if len(files) == 0 { //case empty directory - hdr, _ := taro.FileInfoHeader(stat, matchedPath) + hdr, _ := tar.FileInfoHeader(stat, matchedPath) hdr.Name = destFile if err := tw.WriteHeader(hdr); err != nil { return err @@ -172,7 +221,7 @@ func recursiveTar(srcBase, srcFile, destBase, destFile string, tw *taro.Writer, return nil } else if stat.Mode()&os.ModeSymlink != 0 { //case soft link - hdr, _ := taro.FileInfoHeader(stat, joinedPath) + hdr, _ := tar.FileInfoHeader(stat, joinedPath) target, err := os.Readlink(joinedPath) if err != nil { return err @@ -185,7 +234,7 @@ func recursiveTar(srcBase, srcFile, destBase, destFile string, tw *taro.Writer, } } else { //case regular file or other file type like pipe - hdr, err := taro.FileInfoHeader(stat, joinedPath) + hdr, err := tar.FileInfoHeader(stat, joinedPath) if err != nil { return err } diff --git a/pkg/util/file_indexer.go b/pkg/util/file_indexer.go index e5f25f4932e..dff9968e254 100644 --- a/pkg/util/file_indexer.go +++ b/pkg/util/file_indexer.go @@ -244,6 +244,61 @@ func RunIndexer(directory string, ignoreRules []string) (ret IndexerRet, err err return ret, nil } +// DeployRunIndexer walks the given directory and returns all of the files that are found, that don't match the ignore criteria +func DeployRunIndexer(directory string, ignoreRules []string) (files []string, err error) { + directory = filepath.FromSlash(directory) + + // check for .gitignore file and add odo-file-index.json to .gitignore + gitIgnoreFile, err := CheckGitIgnoreFile(directory) + if err != nil { + return files, err + } + + // add odo-file-index.json path to .gitignore + err = AddOdoFileIndex(gitIgnoreFile) + if err != nil { + return files, err + } + + // Create a function to be passed as a parameter to filepath.Walk + walk := func(fn string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + if fi.IsDir() { + + // if folder is the root folder, don't add it + if fn == directory { + return nil + } + + match, err := IsGlobExpMatch(fn, ignoreRules) + if err != nil { + return err + } + // the folder matches a glob rule and thus should be skipped + if match { + return filepath.SkipDir + } + + if fi.Name() == fileIndexDirectory || fi.Name() == ".git" { + klog.V(4).Info(".odo or .git directory detected, skipping it") + return filepath.SkipDir + } + } + + files = append(files, fn) + return nil + } + + err = filepath.Walk(directory, walk) + if err != nil { + return files, err + } + + return files, nil +} + // CalculateFileDataKeyFromPath converts an absolute path to relative (and converts to OS-specific paths) for use // as a map key in IndexerRet and FileIndex func CalculateFileDataKeyFromPath(absolutePath string, rootDirectory string) (string, error) { diff --git a/pkg/util/util.go b/pkg/util/util.go index 99defd55425..de0af6d26ed 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -1011,6 +1011,54 @@ func DownloadFileWithCache(params DownloadParams, cacheFor int) error { return nil } +// Load a file into memory (http(s):// or file://) +func LoadFileIntoMemory(URL string) (fileBytes []byte, err error) { + // check if we have a file url + if strings.HasPrefix(strings.ToLower(URL), "file://") { + // strip off the "file://" to get a local filepath + filepath := strings.Replace(URL, "file://", "", -1) + + // if filepath doesn't start with a "/"" then we have a relative + // filepath and will need to prepend the current working directory + if !strings.HasPrefix(filepath, "/") { + // get the current working directory + cwd, err := os.Getwd() + if err != nil { + return nil, errors.New("unable to determine current working directory") + } + // prepend the current working directory to the relatove filepath + filepath = fmt.Sprintf("%s/%s", cwd, filepath) + } + + // check to see if filepath exists + info, err := os.Stat(filepath) + if os.IsNotExist(err) || info.IsDir() { + return nil, errors.New(fmt.Sprintf("unable to read file: %s, %s", URL, err)) + } + + // read the bytes from the filepath + fileBytes, err = ioutil.ReadFile(filepath) + if err != nil { + return nil, errors.New(fmt.Sprintf("unable to read file: %s, %s", URL, err)) + } + + return fileBytes, nil + } else { + // assume we have an http:// or https:// url and validate it + err = ValidateURL(URL) + if err != nil { + return nil, errors.New(fmt.Sprintf("invalid url: %s, %s", URL, err)) + } + + // download the file and store the bytes + fileBytes, err = DownloadFileInMemory(HTTPRequestParams{URL: URL}) + if err != nil { + return nil, errors.New(fmt.Sprintf("unable to download url: %s, %s", URL, err)) + } + return fileBytes, nil + } +} + // DownloadFile downloads the file to the filepath given URL and token (if applicable) func DownloadFile(params DownloadParams) error { return DownloadFileWithCache(params, 0) @@ -1095,6 +1143,66 @@ func ValidateURL(sourceURL string) error { return nil } +// ValidateDockerfile validates the string passed through has a FROM on it's first non-whitespace/commented line +// This function could be expanded to be a more viable linter +func ValidateDockerfile(contents []byte) error { + if len(contents) == 0 { + return errors.New("aplha.build-dockerfile URL provided in the Devfile is referencing an empty file") + } + // Split the file downloaded line-by-line + splitContents := strings.Split(string(contents), "\n") + // The first line in a Dockerfile must be a 'FROM', whitespace, or a comment ('#') + // If it there is whitespace, or there are comments, keep checking until we find either a FROM, or something else + // If there is something other than a FROM, the file downloaded wasn't a valid Dockerfile + for _, line := range splitContents { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "#") { + continue + } + if line == "" { + continue + } + if strings.HasPrefix(line, "FROM") { + return nil + } + return errors.New("dockerfile URL provided in the Devfile does not reference a valid Dockerfile") + } + // Would only reach this return statement if splitContents is 0 + return errors.New("dockerfile URL provided in the Devfile does not reference a valid Dockerfile") +} + +// ValidateTag validates the string that has been passed as a tag meets the requirements of a tag +func ValidateTag(tag string) error { + var splitTag = strings.Split(tag, "/") + if len(splitTag) != 3 { + return errors.New("invalid tag: odo deploy reguires a tag in the format /namespace>/") + } + + // Valid characters for the registry, namespace, and image name + characterMatch := regexp.MustCompile(`[a-zA-Z0-9\.\-:_]{4,128}`) + for _, element := range splitTag { + if len(element) < 4 { + return errors.New("invalid tag: " + element + " in the tag is too short. Each element needs to be at least 4 characters.") + } + + if len(element) > 128 { + return errors.New("invalid tag: " + element + " in the tag is too long. Each element cannot be longer than 128.") + } + + // Check that the whole string matches the regular expression + // Match.String was returning a match even when only part of the string is working + if characterMatch.FindString(element) != element { + return errors.New("invalid tag: " + element + " in the tag contains an illegal character. It must only contain alphanumerical values, periods, colons, underscores, and dashes.") + } + + // The registry, namespace, and image, cannot end in '.', '-', '_',or ':' + if strings.HasSuffix(element, ".") || strings.HasSuffix(element, "-") || strings.HasSuffix(element, ":") || strings.HasSuffix(element, "_") { + return errors.New("invalid tag: " + element + " in the tag has an invalid final character. It must end in an alphanumeric value.") + } + } + return nil +} + // ValidateFile validates the file func ValidateFile(filePath string) error { // Check if the file path exist diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go index e40aa0333f6..2afd10f95d1 100644 --- a/pkg/util/util_test.go +++ b/pkg/util/util_test.go @@ -1,6 +1,7 @@ package util import ( + "bytes" "fmt" "io/ioutil" "net" @@ -1695,6 +1696,78 @@ func TestDownloadFileInMemory(t *testing.T) { } } +func TestLoadFileIntoMemory(t *testing.T) { + // Start a local HTTP server + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + // Send response to be tested + _, err := rw.Write([]byte("OK")) + if err != nil { + t.Error(err) + } + })) + + // Close the server when test finishes + defer server.Close() + + tests := []struct { + name string + url string + contains []byte + expectedError string + }{ + { + name: "Case 1: Input url is valid", + url: server.URL, + contains: []byte{79, 75}, + expectedError: "", + }, + { + name: "Case 2: Input url is invalid", + url: "invalid", + contains: []byte(nil), + expectedError: "invalid url:", + }, + { + name: "Case 3: Input http:// url doesnt exist", + url: "http://test.it.doesnt/exist/", + contains: []byte(nil), + expectedError: "unable to download url", + }, + { + name: "Case 4: Input file:// url doesnt exist", + url: "file://./notexists.txt", + contains: []byte(nil), + expectedError: "unable to read file", + }, + { + name: "Case 5: Input file://./util.go exists", + url: "file://./util.go", + contains: []byte("Load a file into memory ("), + expectedError: "", + }, + { + name: "Case 5: Input url is empty", + url: "", + contains: []byte(nil), + expectedError: "invalid url:", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := LoadFileIntoMemory(tt.url) + + if err != nil && !strings.Contains(err.Error(), tt.expectedError) { + t.Errorf("Got err: %s, expected err %s", err.Error(), tt.expectedError) + } + + if tt.expectedError == "" && !bytes.Contains(data, tt.contains) { + t.Errorf("Got: %v, should contain: %v", data, tt.contains) + } + }) + } +} + /* func TestGetGitHubZipURL(t *testing.T) { startPoint := "1.0.0" @@ -1965,3 +2038,173 @@ func TestSliceContainsString(t *testing.T) { }) } } + +func TestDownloadInMemory(t *testing.T) { + tests := []struct { + name string + url string + want bool + }{ + { + name: "Case 1: valid URL", + url: "https://github.com/openshift/odo/blob/master/tests/examples/source/devfiles/nodejs/devfile.yaml", + want: true, + }, + { + name: "Case 2: invalid URL", + url: "https://this/is/not/a/valid/url", + want: false, + }, + { + name: "Case 3: empty URL", + url: "", + want: false, + }, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + _, err := DownloadFileInMemory(HTTPRequestParams{URL: tt.url}) + + got := err == nil + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Got: %v, want: %v", got, tt.want) + t.Logf("Error message is: %v", err) + } + }) + } +} + +func TestValidateDockerfile(t *testing.T) { + tests := []struct { + name string + path string + want bool + }{ + { + name: "Case 1: valid Dockerfile", + path: filepath.Join("tests", "examples", "source", "dockerfiles", "Dockerfile"), + want: true, + }, + { + name: "Case 2: valid Dockerfile with comment", + path: filepath.Join("tests", "examples", "source", "dockerfiles", "DockerfileWithComment"), + want: true, + }, + { + name: "Case 3: valid Dockerfile with whitespace", + path: filepath.Join("tests", "examples", "source", "dockerfiles", "DockerfileWithWhitespace"), + want: true, + }, + { + name: "Case 4: invalid Dockerfile with missing FROM", + path: filepath.Join("tests", "examples", "source", "dockerfiles", "DockerfileInvalid"), + want: false, + }, + { + name: "Case 5: invalid Dockerfile with entry before FROM", + path: filepath.Join("tests", "examples", "source", "dockerfiles", "DockerfileInvalidFROM"), + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Get path for this file (util_test) + _, filename, _, _ := runtime.Caller(0) + // Read the file using a path relative to this file + content, err := ioutil.ReadFile(filepath.Join(filename, "..", "..", "..", tt.path)) + if err != nil { + t.Error("Error when reading the dockerfile: ", err) + } + + err = ValidateDockerfile(content) + + got := err == nil + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Got: %v, want: %v", got, tt.want) + t.Logf("Error message is: %v", err) + } + }) + } +} + +func TestValidateTag(t *testing.T) { + tests := []struct { + name string + tag string + want bool + }{ + { + name: "Case 1: Valid tag ", + tag: "image-registry.openshift-image-registry.svc:5000/default/my-nodejs:1.0", + want: true, + }, + { + name: "Case 2: Invalid tag with trailing period", + tag: "image-registry.openshift-image-registry.svc:5000./default/my-nodejs:1.0", + want: false, + }, + { + name: "Case 3: Invalid tag with trailing dash", + tag: "image-registry.openshift-image-registry.svc:5000-/default/my-nodejs:1.0", + want: false, + }, + { + name: "Case 4: Invalid tag with trailing underscore", + tag: "image-registry.openshift-image-registry.svc:5000_/default/my-nodejs:1.0", + want: false, + }, + { + name: "Case 5: Invalid tag with trailing colon", + tag: "image-registry.openshift-image-registry.svc:5000:/default/my-nodejs:1.0", + want: false, + }, + { + name: "Case 6: Invalid tag with invalid characters", + tag: "imag|||\\e-registry.openshift&^%-image-registry.svc:5000/default!/my-nodejs:1.0", + want: false, + }, + { + name: "Case 7: Missing registry", + tag: "/default/my-nodejs:1.0", + want: false, + }, + { + name: "Case 8: Missing namespace", + tag: "image-registry.openshift-image-registry.svc:5000//my-nodejs:1.0", + want: false, + }, + { + name: "Case 9: Missing image", + tag: "image-registry.openshift-image-registry.svc:5000/default/", + want: false, + }, + { + name: "Case 10: Too many /'s", + tag: "image-registry.openshift/image-registry.svc:5000:/default/my-nodejs:1.0", + want: false, + }, + { + name: "Case 11: Too few /'s", + tag: "image-registry.openshift-image-registry.svc:5000:/default-my-nodejs:1.0", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateTag(tt.tag) + + got := err == nil + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Got: %v, want: %v", got, tt.want) + t.Logf("Error message is: %v", err) + } + }) + } +} diff --git a/tests/examples/source/devfiles/nodejs/devfile_deploy.yaml b/tests/examples/source/devfiles/nodejs/devfile_deploy.yaml new file mode 100644 index 00000000000..22467e00533 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile_deploy.yaml @@ -0,0 +1,44 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs + version: 1.0.0 + alpha.build-dockerfile: "https://raw.githubusercontent.com/groeges/devfile-registry/master/devfiles/nodejs/Dockerfile" + alpha.deployment-manifest: "https://raw.githubusercontent.com/neeraj-laad/nodejs-stack-registry/build-deploy/devfiles/nodejs-basic/deploy/k8s-deploy.yaml" +projects: + - name: express + git: + location: https://github.com/neeraj-laad/nodejs-basic-starter.git +components: + - container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + mountSources: true + name: node12 + memoryLimit: 1024Mi + endpoints: + - name: nodejs + targetPort: 3000 + configuration: + protocol: tcp + scheme: http + type: terminal + - name: debug + targetPort: 9229 + configuration: + protocol: tcp + scheme: http + type: terminal +commands: + - exec: + id: download app dependencies + commandLine: "npm install" + component: node12 + workingDir: ${CHE_PROJECTS_ROOT}/express + group: + kind: build + - exec: + id: run the app + commandLine: "npm start" + component: node12 + workingDir: ${CHE_PROJECTS_ROOT}/express + group: + kind: run diff --git a/tests/examples/source/devfilesV2/nodejs/devfile-no-dockerfile.yaml b/tests/examples/source/devfilesV2/nodejs/devfile-no-dockerfile.yaml new file mode 100644 index 00000000000..6659f6e65d3 --- /dev/null +++ b/tests/examples/source/devfilesV2/nodejs/devfile-no-dockerfile.yaml @@ -0,0 +1,35 @@ +schemaVersion: "2.0.0" +metadata: + name: test-devfile + alpha.deployment-manifest: "https://raw.githubusercontent.com/groeges/devfile-registry/master/devfiles/nodejs/deploy_deployment.yaml" +projects: + - name: nodejs-web-app + git: + location: "https://github.com/che-samples/web-nodejs-sample.git" +components: + - container: + image: quay.io/eclipse/che-nodejs10-ubi:nightly + mountSources: true + name: "runtime" + memoryLimit: 1024Mi + env: + - name: FOO + value: "bar" + endpoints: + - name: '3000/tcp' + targetPort: 3000 +commands: + - exec: + id: download dependencies + commandLine: "npm install" + component: runtime + workingDir: ${CHE_PROJECTS_ROOT}/nodejs-web-app/app + group: + kind: build + - exec: + id: run the app + commandLine: "nodemon app.js" + component: runtime + workingDir: ${CHE_PROJECTS_ROOT}/nodejs-web-app/app + group: + kind: run diff --git a/tests/examples/source/devfilesV2/nodejs/devfile-no-manifest.yaml b/tests/examples/source/devfilesV2/nodejs/devfile-no-manifest.yaml new file mode 100644 index 00000000000..7ebe718df0c --- /dev/null +++ b/tests/examples/source/devfilesV2/nodejs/devfile-no-manifest.yaml @@ -0,0 +1,35 @@ +schemaVersion: "2.0.0" +metadata: + name: test-devfile + alpha.build-dockerfile: "https://raw.githubusercontent.com/neeraj-laad/nodejs-stack-registry/build-deploy/devfiles/nodejs-basic/build/Dockerfile" +projects: + - name: nodejs-web-app + git: + location: "https://github.com/che-samples/web-nodejs-sample.git" +components: + - container: + image: quay.io/eclipse/che-nodejs10-ubi:nightly + mountSources: true + name: "runtime" + memoryLimit: 1024Mi + env: + - name: FOO + value: "bar" + endpoints: + - name: '3000/tcp' + targetPort: 3000 +commands: + - exec: + id: download dependencies + commandLine: "npm install" + component: runtime + workingDir: ${CHE_PROJECTS_ROOT}/nodejs-web-app/app + group: + kind: build + - exec: + id: run the app + commandLine: "nodemon app.js" + component: runtime + workingDir: ${CHE_PROJECTS_ROOT}/nodejs-web-app/app + group: + kind: run diff --git a/tests/examples/source/devfilesV2/nodejs/devfile.yaml b/tests/examples/source/devfilesV2/nodejs/devfile.yaml new file mode 100644 index 00000000000..de4b5ae3b1d --- /dev/null +++ b/tests/examples/source/devfilesV2/nodejs/devfile.yaml @@ -0,0 +1,36 @@ +schemaVersion: "2.0.0" +metadata: + name: test-devfile + alpha.build-dockerfile: "https://raw.githubusercontent.com/neeraj-laad/nodejs-stack-registry/build-deploy/devfiles/nodejs-basic/build/Dockerfile" + alpha.deployment-manifest: "https://raw.githubusercontent.com/groeges/devfile-registry/master/devfiles/nodejs/deploy_deployment.yaml" +projects: + - name: nodejs-web-app + git: + location: "https://github.com/che-samples/web-nodejs-sample.git" +components: + - container: + image: quay.io/eclipse/che-nodejs10-ubi:nightly + mountSources: true + name: "runtime" + memoryLimit: 1024Mi + env: + - name: FOO + value: "bar" + endpoints: + - name: '3000/tcp' + targetPort: 3000 +commands: + - exec: + id: download dependencies + commandLine: "npm install" + component: runtime + workingDir: ${CHE_PROJECTS_ROOT}/nodejs-web-app/app + group: + kind: build + - exec: + id: run the app + commandLine: "nodemon app.js" + component: runtime + workingDir: ${CHE_PROJECTS_ROOT}/nodejs-web-app/app + group: + kind: run diff --git a/tests/examples/source/devfilesV2/nodejs/manifest.yaml b/tests/examples/source/devfilesV2/nodejs/manifest.yaml new file mode 100644 index 00000000000..f58ebc7c28a --- /dev/null +++ b/tests/examples/source/devfilesV2/nodejs/manifest.yaml @@ -0,0 +1 @@ +{"apiVersion":"app.stacks/v1beta1","kind":"RuntimeComponent","metadata":{"creationTimestamp":"2020-06-25T10:02:57Z","generation":1,"labels":{"component":"nodejs-deploy"},"name":"nodejs-deploy","namespace":"default","resourceVersion":"67345259","selfLink":"/apis/app.stacks/v1beta1/namespaces/default/runtimecomponents/nodejs-deploy","uid":"b4eb0bd0-88dc-4827-854f-ec2cf5e4a283"},"spec":{"applicationImage":"image-registry.openshift-image-registry.svc:5000/default/deploy-test:1.0","expose":true,"service":{"port":3000,"type":"ClusterIP"},"storage":{"mountPath":"/logs","size":"2Gi"}}} diff --git a/tests/examples/source/dockerfiles/Dockerfile b/tests/examples/source/dockerfiles/Dockerfile new file mode 100644 index 00000000000..7d1e4cc3bfe --- /dev/null +++ b/tests/examples/source/dockerfiles/Dockerfile @@ -0,0 +1,24 @@ +FROM node:12 + +COPY . /project/ + +# Removing node_modules +RUN rm -rf /project/node_modules + +# Install user-app dependencies +WORKDIR /project +RUN npm install --production + +# Copy the dependencies into a slim Node docker image +FROM node:12-slim + +# Copy project with dependencies +COPY --chown=node:node --from=0 /project /project + +WORKDIR /project + +ENV NODE_ENV production + +USER node + +CMD ["npm", "start"] \ No newline at end of file diff --git a/tests/examples/source/dockerfiles/DockerfileInvalid b/tests/examples/source/dockerfiles/DockerfileInvalid new file mode 100644 index 00000000000..84540406cd9 --- /dev/null +++ b/tests/examples/source/dockerfiles/DockerfileInvalid @@ -0,0 +1,26 @@ +Illegal Line + +FROM node:12 + +COPY . /project/ + +# Removing node_modules +RUN rm -rf /project/node_modules + +# Install user-app dependencies +WORKDIR /project +RUN npm install --production + +# Copy the dependencies into a slim Node docker image +FROM node:12-slim + +# Copy project with dependencies +COPY --chown=node:node --from=0 /project /project + +WORKDIR /project + +ENV NODE_ENV production + +USER node + +CMD ["npm", "start"] \ No newline at end of file diff --git a/tests/examples/source/dockerfiles/DockerfileInvalidFROM b/tests/examples/source/dockerfiles/DockerfileInvalidFROM new file mode 100644 index 00000000000..fbaf514d538 --- /dev/null +++ b/tests/examples/source/dockerfiles/DockerfileInvalidFROM @@ -0,0 +1,24 @@ +FR0M node:12 + +COPY . /project/ + +# Removing node_modules +RUN rm -rf /project/node_modules + +# Install user-app dependencies +WORKDIR /project +RUN npm install --production + +# Copy the dependencies into a slim Node docker image +FROM node:12-slim + +# Copy project with dependencies +COPY --chown=node:node --from=0 /project /project + +WORKDIR /project + +ENV NODE_ENV production + +USER node + +CMD ["npm", "start"] \ No newline at end of file diff --git a/tests/examples/source/dockerfiles/DockerfileWithComment b/tests/examples/source/dockerfiles/DockerfileWithComment new file mode 100644 index 00000000000..bb2fe545776 --- /dev/null +++ b/tests/examples/source/dockerfiles/DockerfileWithComment @@ -0,0 +1,25 @@ +# Install the app dependencies in a full Node docker image +FROM node:12 + +COPY . /project/ + +# Removing node_modules +RUN rm -rf /project/node_modules + +# Install user-app dependencies +WORKDIR /project +RUN npm install --production + +# Copy the dependencies into a slim Node docker image +FROM node:12-slim + +# Copy project with dependencies +COPY --chown=node:node --from=0 /project /project + +WORKDIR /project + +ENV NODE_ENV production + +USER node + +CMD ["npm", "start"] \ No newline at end of file diff --git a/tests/examples/source/dockerfiles/DockerfileWithWhitespace b/tests/examples/source/dockerfiles/DockerfileWithWhitespace new file mode 100644 index 00000000000..1d0b07563d4 --- /dev/null +++ b/tests/examples/source/dockerfiles/DockerfileWithWhitespace @@ -0,0 +1,28 @@ + + + + + FROM node:12 + +COPY . /project/ + +# Removing node_modules +RUN rm -rf /project/node_modules + +# Install user-app dependencies +WORKDIR /project +RUN npm install --production + +# Copy the dependencies into a slim Node docker image +FROM node:12-slim + +# Copy project with dependencies +COPY --chown=node:node --from=0 /project /project + +WORKDIR /project + +ENV NODE_ENV production + +USER node + +CMD ["npm", "start"] \ No newline at end of file diff --git a/tests/examples/source/manifests/deploy_appsody.yaml b/tests/examples/source/manifests/deploy_appsody.yaml new file mode 100644 index 00000000000..19c74051db6 --- /dev/null +++ b/tests/examples/source/manifests/deploy_appsody.yaml @@ -0,0 +1,32 @@ +apiVersion: appsody.dev/v1beta1 +kind: AppsodyApplication +metadata: + name: {{.COMPONENT_NAME}} +spec: + applicationImage: {{.CONTAINER_IMAGE}} + createKnativeService: false + expose: true + livenessProbe: + failureThreshold: 12 + httpGet: + path: /live + port: {{.PORT}} + initialDelaySeconds: 5 + periodSeconds: 2 + monitoring: + labels: + k8s-app: {{.COMPONENT_NAME}} + readinessProbe: + failureThreshold: 12 + httpGet: + path: /ready + port: {{.PORT}} + initialDelaySeconds: 5 + periodSeconds: 2 + timeoutSeconds: 1 + service: + annotations: + prometheus.io/scrape: "true" + port: {{.PORT}} + type: NodePort + version: 1.0.0 diff --git a/tests/examples/source/manifests/deploy_appsody_knative.yaml b/tests/examples/source/manifests/deploy_appsody_knative.yaml new file mode 100644 index 00000000000..8a73bff6dce --- /dev/null +++ b/tests/examples/source/manifests/deploy_appsody_knative.yaml @@ -0,0 +1,32 @@ +apiVersion: appsody.dev/v1beta1 +kind: AppsodyApplication +metadata: + name: {{.COMPONENT_NAME}} +spec: + applicationImage: {{.CONTAINER_IMAGE}} + createKnativeService: true + expose: true + livenessProbe: + failureThreshold: 12 + httpGet: + path: /live + port: {{.PORT}} + initialDelaySeconds: 5 + periodSeconds: 2 + monitoring: + labels: + k8s-app: {{.COMPONENT_NAME}} + readinessProbe: + failureThreshold: 12 + httpGet: + path: /ready + port: {{.PORT}} + initialDelaySeconds: 5 + periodSeconds: 2 + timeoutSeconds: 1 + service: + annotations: + prometheus.io/scrape: "true" + port: {{.PORT}} + type: NodePort + version: 1.0.0 diff --git a/tests/examples/source/manifests/deploy_deployment_clusterip.yaml b/tests/examples/source/manifests/deploy_deployment_clusterip.yaml new file mode 100644 index 00000000000..d55c22782e5 --- /dev/null +++ b/tests/examples/source/manifests/deploy_deployment_clusterip.yaml @@ -0,0 +1,52 @@ +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: {{.COMPONENT_NAME}} +spec: + replicas: 1 + selector: + matchLabels: + app: {{.COMPONENT_NAME}} + template: + metadata: + creationTimestamp: null + labels: + app: {{.COMPONENT_NAME}} + spec: + containers: + - name: {{.COMPONENT_NAME}} + image: {{.CONTAINER_IMAGE}} + ports: + - name: http + containerPort: {{.PORT}} + protocol: TCP +--- +kind: Service +apiVersion: v1 +metadata: + name: {{.COMPONENT_NAME}} +spec: + ports: + - protocol: TCP + port: {{.PORT}} + targetPort: {{.PORT}} + selector: + app: {{.COMPONENT_NAME}} + type: ClusterIP + sessionAffinity: None +--- +kind: Route +apiVersion: route.openshift.io/v1 +metadata: + name: {{.COMPONENT_NAME}} + annotations: + openshift.io/host.generated: 'true' +spec: + to: + kind: Service + name: {{.COMPONENT_NAME}} + weight: 100 + port: + targetPort: {{.PORT}} + wildcardPolicy: None diff --git a/tests/examples/source/manifests/deploy_deployment_no_port_substitution.yaml b/tests/examples/source/manifests/deploy_deployment_no_port_substitution.yaml new file mode 100644 index 00000000000..d095f480746 --- /dev/null +++ b/tests/examples/source/manifests/deploy_deployment_no_port_substitution.yaml @@ -0,0 +1,52 @@ +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: {{.COMPONENT_NAME}} +spec: + replicas: 1 + selector: + matchLabels: + app: {{.COMPONENT_NAME}} + template: + metadata: + creationTimestamp: null + labels: + app: {{.COMPONENT_NAME}} + spec: + containers: + - name: {{.COMPONENT_NAME}} + image: {{.CONTAINER_IMAGE}} + ports: + - name: http + containerPort: 3000 + protocol: TCP +--- +kind: Service +apiVersion: v1 +metadata: + name: {{.COMPONENT_NAME}} +spec: + ports: + - protocol: TCP + port: 3000 + targetPort: 3000 + selector: + app: {{.COMPONENT_NAME}} + type: ClusterIP + sessionAffinity: None +--- +kind: Route +apiVersion: route.openshift.io/v1 +metadata: + name: {{.COMPONENT_NAME}} + annotations: + openshift.io/host.generated: 'true' +spec: + to: + kind: Service + name: {{.COMPONENT_NAME}} + weight: 100 + port: + targetPort: 3000 + wildcardPolicy: None diff --git a/tests/examples/source/manifests/deploy_deployment_nodeport.yaml b/tests/examples/source/manifests/deploy_deployment_nodeport.yaml new file mode 100644 index 00000000000..e51fc37756e --- /dev/null +++ b/tests/examples/source/manifests/deploy_deployment_nodeport.yaml @@ -0,0 +1,52 @@ +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: {{.COMPONENT_NAME}} +spec: + replicas: 1 + selector: + matchLabels: + app: {{.COMPONENT_NAME}} + template: + metadata: + creationTimestamp: null + labels: + app: {{.COMPONENT_NAME}} + spec: + containers: + - name: {{.COMPONENT_NAME}} + image: {{.CONTAINER_IMAGE}} + ports: + - name: http + containerPort: {{.PORT}} + protocol: TCP +--- +kind: Service +apiVersion: v1 +metadata: + name: {{.COMPONENT_NAME}} +spec: + ports: + - protocol: TCP + port: {{.PORT}} + targetPort: {{.PORT}} + nodePort: 30007 + selector: + app: {{.COMPONENT_NAME}} + type: NodePort +--- +kind: Route +apiVersion: route.openshift.io/v1 +metadata: + name: {{.COMPONENT_NAME}} + annotations: + openshift.io/host.generated: 'true' +spec: + to: + kind: Service + name: {{.COMPONENT_NAME}} + weight: 100 + port: + targetPort: {{.PORT}} + wildcardPolicy: None \ No newline at end of file diff --git a/tests/examples/source/manifests/deploy_knative.yaml b/tests/examples/source/manifests/deploy_knative.yaml new file mode 100644 index 00000000000..6fc2e5edad0 --- /dev/null +++ b/tests/examples/source/manifests/deploy_knative.yaml @@ -0,0 +1,9 @@ +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: {{.COMPONENT_NAME}} +spec: + template: + spec: + containers: + - image: {{.CONTAINER_IMAGE}} diff --git a/tests/examples/source/manifests/deploy_runtimecomponent.yaml b/tests/examples/source/manifests/deploy_runtimecomponent.yaml new file mode 100644 index 00000000000..5e24f849978 --- /dev/null +++ b/tests/examples/source/manifests/deploy_runtimecomponent.yaml @@ -0,0 +1,13 @@ +apiVersion: app.stacks/v1beta1 +kind: RuntimeComponent +metadata: + name: {{.COMPONENT_NAME}} +spec: + applicationImage: {{.CONTAINER_IMAGE}} + service: + type: ClusterIP + port: PORT + expose: true + storage: + size: 2Gi + mountPath: "/logs" diff --git a/tests/helper/helper_oc.go b/tests/helper/helper_oc.go index 0021f882cff..212f77b7380 100644 --- a/tests/helper/helper_oc.go +++ b/tests/helper/helper_oc.go @@ -109,6 +109,19 @@ func (oc OcRunner) GetComponentDC(component string, app string, project string) return "" } +func (oc OcRunner) GetInternalRegistryURL() string { + session := CmdRunner(oc.path, "get", "svc", + "-l", "docker-registry", + "--all-namespaces", + "-o=jsonpath={range .items[0]}{@.spec.clusterIP}:{@.spec.ports[0].port}") + + session.Wait() + if session.ExitCode() == 0 { + return string(session.Out.Contents()) + } + return "" +} + // SourceTest checks the component-source-type and the source url in the annotation of the bc and dc // appTestName is the name of the app // sourceType is the type of the source of the component i.e git/binary/local @@ -286,10 +299,10 @@ func (oc OcRunner) SourceLocationBC(componentName string, appName string, projec return sourceLocation } -// checkForImageStream checks if there is a ImageStram with name and tag in openshift namespace -func (oc OcRunner) checkForImageStream(name string, tag string) bool { +// CheckForImageStream checks if there is a ImageStram with name and tag in the specified namespace +func (oc OcRunner) CheckForImageStream(namespace string, name string, tag string) bool { // first check if there is ImageStream with given name - names := strings.Trim(CmdShouldPass(oc.path, "get", "is", "-n", "openshift", + names := strings.Trim(CmdShouldPass(oc.path, "get", "is", "-n", namespace, "-o", "jsonpath='{range .items[*]}{.metadata.name}{\"\\n\"}{end}'"), "'") scanner := bufio.NewScanner(strings.NewReader(names)) namePresent := false @@ -301,8 +314,8 @@ func (oc OcRunner) checkForImageStream(name string, tag string) bool { tagPresent := false // if there is a ImageStream check if there is a given tag if namePresent { - tags := strings.Trim(CmdShouldPass(oc.path, "get", "is", name, "-n", "openshift", - "-o", "jsonpath='{range .spec.tags[*]}{.name}{\"\\n\"}{end}'"), "'") + tags := strings.Trim(CmdShouldPass(oc.path, "get", "is", name, "-n", namespace, + "-o", "jsonpath='{range .status.tags[*]}{.tag}{\"\\n\"}{end}'"), "'") scanner := bufio.NewScanner(strings.NewReader(tags)) for scanner.Scan() { if scanner.Text() == tag { @@ -327,7 +340,7 @@ func (oc OcRunner) ImportImageFromRegistry(registry, image, cmpType, project str // ImportJavaIS import the openjdk image which is used for jars func (oc OcRunner) ImportJavaIS(project string) { // if ImageStram already exists, no need to do anything - if oc.checkForImageStream("java", "8") { + if oc.CheckForImageStream("openshift", "java", "8") { return } @@ -342,7 +355,7 @@ func (oc OcRunner) ImportJavaIS(project string) { // ImportDotnet20IS import the dotnet image func (oc OcRunner) ImportDotnet20IS(project string) { // if ImageStram already exists, no need to do anything - if oc.checkForImageStream("dotnet", "2.0") { + if oc.CheckForImageStream("openshift", "dotnet", "2.0") { return } diff --git a/tests/integration/devfile/cmd_devfile_deploy_delete_test.go b/tests/integration/devfile/cmd_devfile_deploy_delete_test.go new file mode 100644 index 00000000000..d6946288672 --- /dev/null +++ b/tests/integration/devfile/cmd_devfile_deploy_delete_test.go @@ -0,0 +1,100 @@ +package devfile + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "github.com/openshift/odo/tests/helper" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("odo devfile deploy delete command tests", func() { + var namespace, context, currentWorkingDirectory, componentName, originalKubeconfig, imageTag string + + // Using program commmand according to cliRunner in devfile + cliRunner := helper.GetCliRunner() + + // This is run after every Spec (It) + var _ = BeforeEach(func() { + SetDefaultEventuallyTimeout(10 * time.Minute) + context = helper.CreateNewContext() + os.Setenv("GLOBALODOCONFIG", filepath.Join(context, "config.yaml")) + + // Devfile commands require experimental mode to be set + helper.CmdShouldPass("odo", "preference", "set", "Experimental", "true") + + originalKubeconfig = os.Getenv("KUBECONFIG") + helper.LocalKubeconfigSet(context) + namespace = cliRunner.CreateRandNamespaceProject() + imageTag = fmt.Sprintf("image-registry.openshift-image-registry.svc:5000/%s/my-nodejs:1.0", namespace) + currentWorkingDirectory = helper.Getwd() + componentName = helper.RandString(6) + helper.Chdir(context) + }) + + // Clean up after the test + // This is run after every Spec (It) + var _ = AfterEach(func() { + cliRunner.DeleteNamespaceProject(namespace) + helper.Chdir(currentWorkingDirectory) + err := os.Setenv("KUBECONFIG", originalKubeconfig) + Expect(err).NotTo(HaveOccurred()) + helper.DeleteDir(context) + os.Unsetenv("GLOBALODOCONFIG") + }) + + Context("when manifest.yaml isnt present in .odo folder", func() { + It("should fail and alert the user that there isn't a manifest.yaml present", func() { + + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, componentName) + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), context) + helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2", "nodejs", "devfile.yaml"), filepath.Join(context, "devfile.yaml")) + + output := helper.CmdShouldFail("odo", "deploy", "delete") + expectedString := "stat .odo/manifest.yaml: no such file or directory" + + helper.MatchAllInOutput(output, []string{expectedString}) + }) + + }) + + Context("when manifest.yaml is present, but deployment doesn't exist", func() { + It("should pass, by deleting the manifest.yaml, but warn the user that deployment doesn't exist ", func() { + + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, componentName) + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), context) + helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2", "nodejs", "devfile.yaml"), filepath.Join(context, "devfile.yaml")) + helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2", "nodejs", "manifest.yaml"), filepath.Join(context, ".odo", "manifest.yaml")) + + helper.CmdShouldPass("odo", "deploy", "delete") + Expect(helper.VerifyFileExists(filepath.Join(context, ".odo", "manifest.yaml"))).To(Equal(false)) + }) + + }) + + Context("when manifest.yaml is present, and deployment exists", func() { + It("should pass, by deleting the manifest.yaml, and deleting deployment in cluster", func() { + + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, componentName) + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), context) + helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2", "nodejs", "devfile.yaml"), filepath.Join(context, "devfile.yaml")) + helper.CmdShouldPass("odo", "url", "create", "--port", "3000") + + err := helper.ReplaceDevfileField("devfile.yaml", "alpha.deployment-manifest", + fmt.Sprintf("file://%s/../../examples/source/manifests/deploy_deployment_clusterip.yaml", currentWorkingDirectory)) + Expect(err).To(BeNil()) + helper.CmdShouldPass("odo", "deploy", "--tag", imageTag) + + helper.CmdShouldPass("odo", "deploy", "delete") + cliRunner.WaitAndCheckForExistence("deployments", namespace, 1) + cliRunner.WaitAndCheckForExistence("services", namespace, 1) + cliRunner.WaitAndCheckForExistence("routes", namespace, 1) + Expect(helper.VerifyFileExists(filepath.Join(context, ".odo", "manifest.yaml"))).To(Equal(false)) + }) + + }) +}) diff --git a/tests/integration/devfile/cmd_devfile_deploy_test.go b/tests/integration/devfile/cmd_devfile_deploy_test.go new file mode 100644 index 00000000000..55340b53171 --- /dev/null +++ b/tests/integration/devfile/cmd_devfile_deploy_test.go @@ -0,0 +1,183 @@ +package devfile + +import ( + "fmt" + "os" + "path/filepath" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/openshift/odo/tests/helper" +) + +var _ = Describe("odo devfile deploy command tests", func() { + var namespace, context, cmpName, currentWorkingDirectory, originalKubeconfig, imageTag string + // Using program commmand according to cliRunner in devfile + cliRunner := helper.GetCliRunner() + oc := helper.NewOcRunner("oc") + + // This is run after every Spec (It) + var _ = BeforeEach(func() { + SetDefaultEventuallyTimeout(10 * time.Minute) + context = helper.CreateNewContext() + os.Setenv("GLOBALODOCONFIG", filepath.Join(context, "config.yaml")) + + // Devfile push requires experimental mode to be set + helper.CmdShouldPass("odo", "preference", "set", "Experimental", "true") + + originalKubeconfig = os.Getenv("KUBECONFIG") + helper.LocalKubeconfigSet(context) + namespace = cliRunner.CreateRandNamespaceProject() + registryURL := oc.GetInternalRegistryURL() + Expect(registryURL).NotTo(Equal("")) + imageTag = fmt.Sprintf("%s/%s/my-nodejs:1.0", registryURL, namespace) + currentWorkingDirectory = helper.Getwd() + cmpName = helper.RandString(6) + helper.Chdir(context) + }) + + var _ = AfterEach(func() { + cliRunner.DeleteNamespaceProject(namespace) + helper.Chdir(currentWorkingDirectory) + err := os.Setenv("KUBECONFIG", originalKubeconfig) + Expect(err).NotTo(HaveOccurred()) + helper.DeleteDir(context) + os.Unsetenv("GLOBALODOCONFIG") + }) + + Context("Verify deploy completes when passing a valid Dockerfile URL from the devfile", func() { + It("Should succesfully download the dockerfile and build the project", func() { + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, cmpName) + helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2", "nodejs", "devfile.yaml"), filepath.Join(context, "devfile.yaml")) + helper.CmdShouldPass("odo", "url", "create", "--port", "3000") + err := helper.ReplaceDevfileField("devfile.yaml", "alpha.deployment-manifest", + fmt.Sprintf("file://%s/../../examples/source/manifests/deploy_deployment_clusterip.yaml", currentWorkingDirectory)) + Expect(err).To(BeNil()) + output := helper.CmdShouldPass("odo", "deploy", "--tag", imageTag) + cliRunner.WaitAndCheckForExistence("buildconfig", namespace, 1) + Expect(output).NotTo(ContainSubstring("does not point to a valid Dockerfile")) + Expect(output).To(ContainSubstring("Successfully built container image")) + Expect(output).To(ContainSubstring("Successfully deployed component")) + }) + }) + + Context("Verify error when dockerfile specified in devfile field doesn't point to a valid Dockerfile", func() { + It("Should error out with 'URL does not point to a valid Dockerfile'", func() { + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, cmpName) + helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2", "nodejs", "devfile.yaml"), filepath.Join(context, "devfile.yaml")) + helper.CmdShouldPass("odo", "url", "create", "--port", "3000") + + err := helper.ReplaceDevfileField("devfile.yaml", "alpha.build-dockerfile", "https://google.com") + Expect(err).To(BeNil()) + + cmdOutput := helper.CmdShouldFail("odo", "deploy", "--tag", imageTag) + Expect(cmdOutput).To(ContainSubstring("does not reference a valid Dockerfile")) + }) + }) + + // This test depends on the nodejs stack to no have a alpha.build-dockerfile field. + // This may not be the case in the future when the stack gets updated. + Context("Verify error when no Dockerfile exists in project and no 'dockerfile' specified in devfile", func() { + It("Should error out with 'dockerfile required for build.'", func() { + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, cmpName) + helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2", "nodejs", "devfile-no-dockerfile.yaml"), filepath.Join(context, "devfile.yaml")) + helper.CmdShouldPass("odo", "url", "create", "--port", "3000") + cmdOutput := helper.CmdShouldFail("odo", "deploy", "--tag", imageTag) + Expect(cmdOutput).To(ContainSubstring("dockerfile required for build. No 'alpha.build-dockerfile' field found in devfile, or Dockerfile found in project directory")) + }) + }) + + Context("Verify error when no manifest definition exists in devfile", func() { + It("Should error out with 'Unable to deploy as alpha.deployment-manifest is not defined in devfile.yaml'", func() { + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, cmpName) + helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2", "nodejs", "devfile-no-manifest.yaml"), filepath.Join(context, "devfile.yaml")) + helper.CmdShouldPass("odo", "url", "create", "--port", "3000") + + cmdOutput := helper.CmdShouldFail("odo", "deploy", "--tag", imageTag) + Expect(cmdOutput).To(ContainSubstring("Unable to deploy as alpha.deployment-manifest is not defined in devfile.yaml")) + }) + }) + + Context("Verify error when invalid manifest definition exists in devfile", func() { + It("Should error out with 'Invalid manifest url'", func() { + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, cmpName) + helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2", "nodejs", "devfile.yaml"), filepath.Join(context, "devfile.yaml")) + helper.CmdShouldPass("odo", "url", "create", "--port", "3000") + err := helper.ReplaceDevfileField("devfile.yaml", "alpha.deployment-manifest", "google.com") + Expect(err).To(BeNil()) + + cmdOutput := helper.CmdShouldFail("odo", "deploy", "--tag", imageTag) + Expect(cmdOutput).To(ContainSubstring("invalid url")) + }) + }) + + Context("Verify error when manifest file doesnt exist on web", func() { + It("Should error out with 'Unable to download manifest'", func() { + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, cmpName) + helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2", "nodejs", "devfile.yaml"), filepath.Join(context, "devfile.yaml")) + helper.CmdShouldPass("odo", "url", "create", "--port", "3000") + err := helper.ReplaceDevfileField("devfile.yaml", "alpha.deployment-manifest", "http://github.com/myfile.yaml") + Expect(err).To(BeNil()) + + cmdOutput := helper.CmdShouldFail("odo", "deploy", "--tag", imageTag) + Expect(cmdOutput).To(ContainSubstring("unable to download url")) + }) + }) + + Context("Verify error if no port is found in env.yaml", func() { + It("Should error out with 'Unable to find `port` for deployment.'", func() { + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, cmpName) + helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2", "nodejs", "devfile.yaml"), filepath.Join(context, "devfile.yaml")) + err := helper.ReplaceDevfileField("devfile.yaml", "alpha.deployment-manifest", + fmt.Sprintf("file://%s/../../examples/source/manifests/deploy_deployment_clusterip.yaml", currentWorkingDirectory)) + Expect(err).To(BeNil()) + + cmdOutput := helper.CmdShouldFail("odo", "deploy", "--tag", imageTag) + Expect(cmdOutput).To(ContainSubstring("unable to find port for URL of kind")) + }) + }) + + Context("Verify deploy completes when no port in env.yaml or PORT in manifest", func() { + It("Should successfully deploy the application and return a URL", func() { + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, cmpName) + helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2", "nodejs", "devfile.yaml"), filepath.Join(context, "devfile.yaml")) + err := helper.ReplaceDevfileField("devfile.yaml", "alpha.deployment-manifest", + fmt.Sprintf("file://%s/../../examples/source/manifests/deploy_deployment_no_port_substitution.yaml", currentWorkingDirectory)) + Expect(err).To(BeNil()) + + cmdOutput := helper.CmdShouldPass("odo", "deploy", "--tag", imageTag) + Expect(cmdOutput).To(ContainSubstring(fmt.Sprintf("Successfully deployed component: http://%s-deploy-%s", cmpName, namespace))) + }) + }) + + Context("Verify deploy completes when using manifest with deployment/service/route", func() { + It("Should successfully deploy the application and return a URL", func() { + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, cmpName) + helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2", "nodejs", "devfile.yaml"), filepath.Join(context, "devfile.yaml")) + helper.CmdShouldPass("odo", "url", "create", "--port", "3000") + err := helper.ReplaceDevfileField("devfile.yaml", "alpha.deployment-manifest", + fmt.Sprintf("file://%s/../../examples/source/manifests/deploy_deployment_clusterip.yaml", currentWorkingDirectory)) + Expect(err).To(BeNil()) + + cmdOutput := helper.CmdShouldPass("odo", "deploy", "--tag", imageTag) + Expect(cmdOutput).To(ContainSubstring(fmt.Sprintf("Successfully deployed component: http://%s-deploy-%s", cmpName, namespace))) + }) + }) + + Context("Verify the deploy completes on openshift with no tag provided", func() { + It("Should successfully deploy the application", func() { + helper.CmdShouldPass("odo", "create", "nodejs", "--project", namespace, cmpName) + helper.CmdShouldPass("odo", "url", "create", "--port", "3000") + helper.CopyExampleDevFile(filepath.Join("source", "devfilesV2", "nodejs", "devfile.yaml"), filepath.Join(context, "devfile.yaml")) + cmdOutput := helper.CmdShouldPass("odo", "deploy") + Expect(cmdOutput).To(ContainSubstring(fmt.Sprintf("Successfully deployed component: http://%s-deploy-%s", cmpName, namespace))) + + output := cliRunner.GetServices(namespace) + Expect(output).To(ContainSubstring(cmpName + "-deploy")) + + ok := oc.CheckForImageStream(namespace, cmpName, "latest") + Expect(ok).To(BeTrue()) + }) + }) +}) diff --git a/vendor/k8s.io/client-go/go.sum b/vendor/k8s.io/client-go/go.sum new file mode 100644 index 00000000000..e8fc0cfb2fb --- /dev/null +++ b/vendor/k8s.io/client-go/go.sum @@ -0,0 +1,211 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest/adal v0.5.0 h1:q2gDruN08/guU9vAjuPWff0+QIrpH6ediguzdAzXAUU= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/date v0.1.0 h1:YGrhWfrgtFs84+h0o46rJrlmsZtyZRg470CqAXTZaGM= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0 h1:Ww5g4zThfD/6cLb4z6xxgeyDa7QDkizMkJKe0ysZXp0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e h1:p1yVGRW3nmb85p1Sh1ZJSDm4A4iKLS5QNbvUHMgGu/M= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903 h1:LbsanbbD6LieFkXbj9YNNBupiGHJgFeLpO0j0Fza1h8= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gophercloud/gophercloud v0.1.0 h1:P/nh25+rzXouhytV2pUHBb65fnds26Ghl8/391+sT5o= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20190821162956-65e3620a7ae7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.17.1 h1:i46MidoDOE9tvQ0TTEYggf3ka/pziP1+tHI/GFVeJao= +k8s.io/api v0.17.1/go.mod h1:zxiAc5y8Ngn4fmhWUtSxuUlkfz1ixT7j9wESokELzOg= +k8s.io/apimachinery v0.17.1 h1:zUjS3szTxoUjTDYNvdFkYt2uMEXLcthcbp+7uZvWhYM= +k8s.io/apimachinery v0.17.1/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=