diff --git a/pkg/components/eventing/sources/github/github.go b/pkg/components/eventing/sources/github/github.go index b9493bfb..7ed3ef56 100644 --- a/pkg/components/eventing/sources/github/github.go +++ b/pkg/components/eventing/sources/github/github.go @@ -19,12 +19,14 @@ package github import ( "fmt" + "knative.dev/reconciler-test/pkg/manifest" + "knative.dev/reconciler-test/pkg/config" "knative.dev/reconciler-test/pkg/framework" ) const ( - artifactURITemplate = "https://github.com/knative/eventing-contrib/releases/download/v%s/github.yaml" + artifactURLTemplate = "https://github.com/knative/eventing-contrib/releases/download/v%s/github.yaml" ) var ( @@ -45,8 +47,8 @@ func (s *githubComponent) Required(rc framework.ResourceContext, cfg config.Conf ghcfg := config.GetConfig(cfg, "components/eventing/sources/github").(GithubConfig) // TODO: validate configuration - - artifactURI := fmt.Sprintf(artifactURITemplate, ghcfg.Version) - rc.CreateFromURIOrFail(artifactURI, false) - + // TODO: check cluster for existing source + artifactURL := fmt.Sprintf(artifactURLTemplate, ghcfg.Version) + rc.Logf("installing GitHubSource release ", ghcfg.Version) + rc.Apply(manifest.FromURL(artifactURL)) } diff --git a/pkg/components/sequencestepper/sequencestepper.go b/pkg/components/sequencestepper/sequencestepper.go index 8c47937d..10bcee94 100644 --- a/pkg/components/sequencestepper/sequencestepper.go +++ b/pkg/components/sequencestepper/sequencestepper.go @@ -19,6 +19,7 @@ package sequencestepper import ( corev1 "k8s.io/api/core/v1" "knative.dev/reconciler-test/pkg/config" + "knative.dev/reconciler-test/pkg/manifest" "knative.dev/reconciler-test/pkg/framework" "knative.dev/reconciler-test/pkg/installer" @@ -36,8 +37,16 @@ func Deploy(rc framework.ResourceContext) corev1.ObjectReference { image := rc.ImageName(packageName) name := helpers.AppendRandomString("seq-stepper") - rc.CreateFromYAMLOrFail(installer.ExecuteTemplate(podTemplate, map[string]interface{}{"Name": name, "Image": image})) - rc.CreateFromYAMLOrFail(installer.ExecuteTemplate(serviceTemplate, map[string]interface{}{"Name": name})) + data := struct { + Name string + Image string + }{ + Name: name, + Image: image, + } + + rc.Apply(manifest.FromString(podTemplate), data) + rc.Apply(manifest.FromString(serviceTemplate), data) return corev1.ObjectReference{ Namespace: rc.Namespace(), diff --git a/pkg/config.go b/pkg/config.go index e76acd57..672824ae 100644 --- a/pkg/config.go +++ b/pkg/config.go @@ -25,5 +25,5 @@ import ( // Can be embedded by downstream projects. type AllConfig struct { framework.BaseConfig - Component components.ComponentConfig + Components components.ComponentConfig } diff --git a/pkg/framework/resourcecontext.go b/pkg/framework/resourcecontext.go index 9d35e0e8..d209aef4 100644 --- a/pkg/framework/resourcecontext.go +++ b/pkg/framework/resourcecontext.go @@ -19,11 +19,14 @@ package framework import ( "context" "encoding/json" + "errors" "fmt" "os" "strings" "time" + "knative.dev/reconciler-test/pkg/installer" + "knative.dev/reconciler-test/pkg/manifest" "k8s.io/apimachinery/pkg/api/meta" @@ -40,12 +43,60 @@ import ( type ResourceContext interface { context.Context + // --- Context getters + // Namespace returns the current namespace Namespace() string // ImageName returns the image name corresponding to the given Go package name ImageName(packageName string) string + // --- Context operations + + // Apply applies the manifest returned by provider. + // When data is set, it instantiates the manifest template + // before applying it. + // When the manifest includes import path references, it + // builds them into Go binaries, containerizes them, publishes them, + // before applying it. Current limitation: can only be used with local, non-templated manifest. + // + // The test is marked as Fail when Apply fails + Apply(provider manifest.Provider, data ...interface{}) + + // ApplyOrError applies the manifest returned by provider. after + // When data is set, it instantiates the manifest template + // before applying it. + // When the manifest includes import path references, it + // builds them into Go binaries, containerizes them, publishes them, + // before applying it. Current limitation: can only be used with local, non-templated manifest. + // + // Returns an error when ApplyOrError fails + ApplyOrError(provider manifest.Provider, data ...interface{}) error + + // Delete deletes the resources returned by provider. + // When data is set, it instantiates the manifest template + // before deleting it. + // The test is marked as Fail when Delete fails + Delete(provider manifest.Provider, data ...interface{}) + + // DeleteOrError deletes the resources returned by provider. + // When data is set, it instantiates the manifest template + // before deleting it. + // Returns an error when DeleteOrError fails + DeleteOrError(provider manifest.Provider, data ...interface{}) error + + // --- Logging, Failures. Subset of testing.T + + Helper() + Log(args ...interface{}) + Logf(format string, args ...interface{}) + Error(args ...interface{}) + Errorf(format string, args ...interface{}) + Fatal(args ...interface{}) + Fatalf(format string, args ...interface{}) + + // --- Deprecation + // Create a resource from the given object (or fail) CreateOrFail(obj runtime.Object) @@ -62,22 +113,6 @@ type ResourceContext interface { // 5. pathname = combination of all previous cases, the string can contain // multiple records (file, directory or url) separated by comma CreateFromURIOrFail(uri string, recursive bool) - - // Delete deletes the resource specified in the given YAML - DeleteFromYAML(yaml string) error - - // Delete deletes the resource specified in the given YAML (or fail) - DeleteFromYAMLOrFail(yaml string) - - // TODO: Get, Update, Apply - - // --- Failures. Subset of testing.T - - Helper() - Error(args ...interface{}) - Errorf(format string, args ...interface{}) - Fatal(args ...interface{}) - Fatalf(format string, args ...interface{}) } // --- Default implementation @@ -103,6 +138,75 @@ func (c *resourceContextImpl) ImageName(packageName string) string { return fmt.Sprintf("%s/%s", repository, parts[len(parts)-1]) } +func (c *resourceContextImpl) Apply(provider manifest.Provider, data ...interface{}) { + c.Helper() + err := c.ApplyOrError(provider, data...) + if err != nil { + c.Fatal(err) + } +} + +func (c *resourceContextImpl) ApplyOrError(provider manifest.Provider, data ...interface{}) error { + c.Helper() + path, err := provider.GetPath() + if err != nil { + return err + } + + if len(data) > 0 { + path, err = installer.ParseTemplates(path, data) + if err != nil { + return err + } + } + + // Attempt to run ko apply -f path + c.Logf("running ko apply -f %s\n", path) + o, err := installer.KoApply(path) + if err != nil { + // We care about the command output more than the exit code + c.Logf("running ko apply -f %s\n", path) + + return errors.New(o) + } + return nil +} + +// Delete deletes the resources returned by provider. +// The test is marked as Fail when Delete fails +func (c *resourceContextImpl) Delete(provider manifest.Provider, data ...interface{}) { + err := c.DeleteOrError(provider, data...) + if err != nil { + c.Fatal(err) + } +} + +// DeleteOrError deletes the resources returned by provider. +// Returns an error when DeleteOrError fails +func (c *resourceContextImpl) DeleteOrError(provider manifest.Provider, data ...interface{}) error { + path, err := provider.GetPath() + if err != nil { + return err + } + + if len(data) > 0 { + path, err = installer.ParseTemplates(path, data) + if err != nil { + return err + } + } + + // Attempt to run ko delete -f path + o, err := installer.KoDelete(path) + if err != nil { + // We care about the command output more than the exit code + return errors.New(o) + } + return nil +} + +// --- Deprecated + func (c *resourceContextImpl) CreateOrFail(obj runtime.Object) { c.Helper() gvr, _ := meta.UnsafeGuessKindToResource(obj.GetObjectKind().GroupVersionKind()) @@ -205,6 +309,14 @@ func (c *resourceContextImpl) Fatalf(format string, args ...interface{}) { c.Errorf(format, args...) } +func (c *resourceContextImpl) Log(args ...interface{}) { + fmt.Println(args...) +} + +func (c *resourceContextImpl) Logf(format string, args ...interface{}) { + fmt.Printf(format, args...) +} + // --- context.Context func (c *resourceContextImpl) Deadline() (deadline time.Time, ok bool) { diff --git a/pkg/framework/suite_runner.go b/pkg/framework/suiterunner.go similarity index 100% rename from pkg/framework/suite_runner.go rename to pkg/framework/suiterunner.go diff --git a/pkg/framework/test_runner.go b/pkg/framework/testrunner.go similarity index 98% rename from pkg/framework/test_runner.go rename to pkg/framework/testrunner.go index b401cd60..c97ca803 100644 --- a/pkg/framework/test_runner.go +++ b/pkg/framework/testrunner.go @@ -126,7 +126,7 @@ func (t *test) Run(fn func(TestContext)) { } // TODO: validate feature to match DNS-1123 label - namespace := helpers.AppendRandomString(strings.ToLower(t.feature)) + namespace := helpers.AppendRandomString(strings.ToLower(t.t.Name())) ctx := withInjection(context.Background()) tc := &testContextImpl{ diff --git a/pkg/installer/cmd.go b/pkg/installer/cmd.go new file mode 100644 index 00000000..f31fd369 --- /dev/null +++ b/pkg/installer/cmd.go @@ -0,0 +1,44 @@ +/* + * Copyright 2020 The Knative Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package installer + +import ( + "os" + "os/exec" + "strings" +) + +// Helper functions to run shell commands. + +func cmd(dir string, cmdLine string) *exec.Cmd { + cmdSplit := strings.Split(cmdLine, " ") + cmd := cmdSplit[0] + args := cmdSplit[1:] + c := exec.Command(cmd, args...) + c.Dir = dir + return c +} + +func runCmd(cmdLine string) (string, error) { + dir, err := os.Getwd() + if err != nil { + return "", err + } + cmd := cmd(dir, cmdLine) + + cmdOut, err := cmd.CombinedOutput() + return string(cmdOut), err +} diff --git a/pkg/installer/ko.go b/pkg/installer/ko.go new file mode 100644 index 00000000..e14c1060 --- /dev/null +++ b/pkg/installer/ko.go @@ -0,0 +1,47 @@ +/* + * Copyright 2020 The Knative Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package installer + +import ( + "fmt" +) + +// Use ko to publish the image. +func KoPublish(path string) (string, error) { + out, err := runCmd(fmt.Sprintf("ko publish %s", path)) + if err != nil { + return "", err + } + return out, nil +} + +// Use ko to apply filename +func KoApply(filename string) (string, error) { + out, err := runCmd(fmt.Sprintf("ko apply -f %s", filename)) + if err != nil { + return out, err + } + return out, nil +} + +// Use ko to delete filename +func KoDelete(filename string) (string, error) { + out, err := runCmd(fmt.Sprintf("ko delete -f %s", filename)) + if err != nil { + return out, err + } + return out, nil +} diff --git a/pkg/installer/templates.go b/pkg/installer/templates.go index 684e0bd4..b7e97e56 100644 --- a/pkg/installer/templates.go +++ b/pkg/installer/templates.go @@ -29,7 +29,7 @@ import ( // ParseTemplates walks through all the template yaml file in the given directory // and produces instantiated yaml file in a temporary directory. // Return the name of the temporary directory -func ParseTemplates(path string, config map[string]interface{}) string { +func ParseTemplates(path string, config interface{}) (string, error) { dir, err := ioutil.TempDir("", "processed_yaml") if err != nil { panic(err) @@ -59,9 +59,9 @@ func ParseTemplates(path string, config map[string]interface{}) string { }) log.Print("new files in ", dir) if err != nil { - panic(err) + return "", err } - return dir + return dir, nil } // ExecuteTemplate instantiates the given template with data diff --git a/pkg/manifest/objprovider.go b/pkg/manifest/objprovider.go new file mode 100644 index 00000000..d16e5f32 --- /dev/null +++ b/pkg/manifest/objprovider.go @@ -0,0 +1,47 @@ +/* + * Copyright 2020 The Knative Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package manifest + +import ( + "io/ioutil" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer/json" +) + +type objProvider struct { + obj runtime.Object +} + +func (p *objProvider) GetPath() (string, error) { + f, err := ioutil.TempFile("", "manifest-*.yaml") + if err != nil { + return "", err + } + defer f.Close() + + serializer := json.NewSerializerWithOptions(json.DefaultMetaFactory, nil, nil, json.SerializerOptions{Yaml: true}) + err = serializer.Encode(p.obj, f) + if err != nil { + return "", nil + } + return f.Name(), nil +} + +func (p *objProvider) Recursive() bool { + return false +} diff --git a/pkg/manifest/providers.go b/pkg/manifest/providers.go new file mode 100644 index 00000000..d43e30ae --- /dev/null +++ b/pkg/manifest/providers.go @@ -0,0 +1,50 @@ +/* + * Copyright 2020 The Knative Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package manifest + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +type Provider interface { + + // GetPath returns the location of the manifest (local file or directory) + GetPath() (string, error) + + // Recursive indicates whether to includes manifests in subdirectories + Recursive() bool +} + +// Create a resource from the given object (or fail) +func FromObject(obj runtime.Object) Provider { + return &objProvider{obj} +} + +// CreateFromYAMLOrFail creates resources from the given YAML specification (or fail) +func FromString(yamlstr string) Provider { + return &stringProvider{str: yamlstr} +} + +// FromURL provides manifest(s) located at the given url. +func FromURLRecursive(url string, recursive bool) Provider { + return &urlProvider{url: url, recursive: recursive} +} + +// FromURL provides manifest(s) located at the given uri. +func FromURL(uri string) Provider { + return FromURLRecursive(uri, false) +} diff --git a/pkg/manifest/stringprovider.go b/pkg/manifest/stringprovider.go new file mode 100644 index 00000000..de46f487 --- /dev/null +++ b/pkg/manifest/stringprovider.go @@ -0,0 +1,46 @@ +/* + * Copyright 2020 The Knative Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package manifest + +import ( + "io/ioutil" +) + +type stringProvider struct { + str string +} + +func (p *stringProvider) GetPath() (string, error) { + // TODO: check string is valid YAML? + + f, err := ioutil.TempFile("", "manifest-*.yaml") + if err != nil { + return "", err + } + defer f.Close() + + _, err = f.WriteString(p.str) + if err != nil { + return "", err + } + + return f.Name(), nil +} + +func (p *stringProvider) Recursive() bool { + return false +} diff --git a/pkg/manifest/urlprovider.go b/pkg/manifest/urlprovider.go new file mode 100644 index 00000000..afc707e0 --- /dev/null +++ b/pkg/manifest/urlprovider.go @@ -0,0 +1,67 @@ +/* + * Copyright 2020 The Knative Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package manifest + +import ( + "io/ioutil" + "net/http" + "net/url" + "path" + "strings" +) + +type urlProvider struct { + url string + recursive bool +} + +func (p *urlProvider) GetPath() (string, error) { + if isURL(p.url) { + // Fetches manifest(s) + resp, err := http.Get(p.url) + if err != nil { + return "", err + } + defer resp.Body.Close() + + u, _ := url.Parse(p.url) // no error (see isURL) + stem := strings.TrimRight(path.Base(u.Path), ".yaml") + + f, err := ioutil.TempFile("", stem+"*.yaml") + if err != nil { + return "", err + } + defer f.Close() + + buffer, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + _, err = f.Write(buffer) + if err != nil { + return "", err + } + return f.Name(), nil + } + + return p.url, nil +} + +func (p *urlProvider) Recursive() bool { + return p.recursive +} diff --git a/pkg/manifest/yaml.go b/pkg/manifest/yaml.go index d107598f..f979667b 100644 --- a/pkg/manifest/yaml.go +++ b/pkg/manifest/yaml.go @@ -41,7 +41,6 @@ import ( // 5. pathname = combination of all previous cases, the string can contain // multiple records (file, directory or url) separated by comma func Parse(pathname string, recursive bool) ([]unstructured.Unstructured, error) { - pathnames := strings.Split(pathname, ",") var aggregated []unstructured.Unstructured for _, pth := range pathnames {