diff --git a/pkg/odo/cli/pipelines/bootstrap.go b/pkg/odo/cli/pipelines/bootstrap.go index 7a1d11e6d2b..5eecea25d00 100644 --- a/pkg/odo/cli/pipelines/bootstrap.go +++ b/pkg/odo/cli/pipelines/bootstrap.go @@ -5,9 +5,9 @@ import ( "strings" "github.com/openshift/odo/pkg/odo/genericclioptions" + "github.com/openshift/odo/pkg/pipelines" "github.com/spf13/cobra" - "github.com/openshift/odo/pkg/pipelines" ktemplates "k8s.io/kubernetes/pkg/kubectl/util/templates" ) @@ -28,10 +28,11 @@ var ( // BootstrapOptions encapsulates the options for the odo pipelines bootstrap // command. type BootstrapOptions struct { - quayUsername string - baseRepo string // e.g. tekton/triggers - prefix string // used to generate the environments in a shared cluster - + quayUsername string + gitRepo string // e.g. tekton/triggers + prefix string // used to generate the environments in a shared cluster + githubToken string + quayIOAuthFilename string // generic context options common to all commands *genericclioptions.Context } @@ -46,9 +47,6 @@ func NewBootstrapOptions() *BootstrapOptions { // If the prefix provided doesn't have a "-" then one is added, this makes the // generated environment names nicer to read. func (bo *BootstrapOptions) Complete(name string, cmd *cobra.Command, args []string) error { - bo.quayUsername = args[0] - bo.baseRepo = args[1] - if bo.prefix != "" && !strings.HasSuffix(bo.prefix, "-") { bo.prefix = bo.prefix + "-" } @@ -58,15 +56,22 @@ func (bo *BootstrapOptions) Complete(name string, cmd *cobra.Command, args []str // Validate validates the parameters of the BootstrapOptions. func (bo *BootstrapOptions) Validate() error { // TODO: this won't work with GitLab as the repo can have more path elements. - if len(strings.Split(bo.baseRepo, "/")) != 2 { - return fmt.Errorf("repo must be org/repo: %s", bo.baseRepo) + if len(strings.Split(bo.gitRepo, "/")) != 2 { + return fmt.Errorf("repo must be org/repo: %s", bo.gitRepo) } return nil } // Run runs the project bootstrap command. func (bo *BootstrapOptions) Run() error { - return pipelines.Bootstrap(bo.quayUsername, bo.baseRepo, bo.prefix) + options := pipelines.BootstrapOptions{ + GithubToken: bo.githubToken, + GitRepo: bo.gitRepo, + Prefix: bo.prefix, + QuayAuthFileName: bo.quayIOAuthFilename, + QuayUserName: bo.quayUsername, + } + return pipelines.Bootstrap(&options) } // NewCmdBootstrap creates the project bootstrap command. @@ -78,12 +83,19 @@ func NewCmdBootstrap(name, fullName string) *cobra.Command { Short: bootstrapShortDesc, Long: bootstrapLongDesc, Example: fmt.Sprintf(bootstrapExample, fullName), - Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { genericclioptions.GenericRun(o, cmd, args) }, } bootstrapCmd.Flags().StringVarP(&o.prefix, "prefix", "p", "", "add a prefix to the environment names") + bootstrapCmd.Flags().StringVar(&o.quayUsername, "quay-username", "", "Image registry username") + bootstrapCmd.MarkFlagRequired("quay-username") + bootstrapCmd.Flags().StringVar(&o.githubToken, "github-token", "", "provide the Github token") + bootstrapCmd.MarkFlagRequired("github-token") + bootstrapCmd.Flags().StringVar(&o.quayIOAuthFilename, "dockerconfigjson", "", "Docker configuration json filename") + bootstrapCmd.MarkFlagRequired("dockerconfigjson") + bootstrapCmd.Flags().StringVar(&o.gitRepo, "git-repository", "", "provide the base repository") + bootstrapCmd.MarkFlagRequired("git-repository") return bootstrapCmd } diff --git a/pkg/odo/cli/pipelines/bootstrap_test.go b/pkg/odo/cli/pipelines/bootstrap_test.go index f0319ff9ba6..375a2ef55ff 100644 --- a/pkg/odo/cli/pipelines/bootstrap_test.go +++ b/pkg/odo/cli/pipelines/bootstrap_test.go @@ -1,24 +1,51 @@ package pipelines import ( + "bytes" "regexp" "testing" "github.com/spf13/cobra" ) +func TestCompleteBootstrapOptions(t *testing.T) { + completeTests := []struct { + name string + prefix string + wantPrefix string + }{ + {"no prefix", "", ""}, + {"prefix with hyphen", "test-", "test-"}, + {"prefix without hyphen", "test", "test-"}, + } + + for _, tt := range completeTests { + o := BootstrapOptions{prefix: tt.prefix} + + err := o.Complete("test", &cobra.Command{}, []string{"test", "test/repo"}) + + if err != nil { + t.Errorf("Complete() %#v failed: ", err) + } + + if o.prefix != tt.wantPrefix { + t.Errorf("Complete() %#v prefix: got %s, want %s", tt.name, o.prefix, tt.wantPrefix) + } + } +} + func TestValidateBootstrapOptions(t *testing.T) { optionTests := []struct { - name string - baseRepo string - errMsg string + name string + gitRepo string + errMsg string }{ {"invalid repo", "test", "repo must be org/repo"}, {"valid repo", "test/repo", ""}, } for _, tt := range optionTests { - o := BootstrapOptions{quayUsername: "testing", baseRepo: tt.baseRepo, prefix: "test"} + o := BootstrapOptions{quayUsername: "testing", gitRepo: tt.gitRepo, prefix: "test"} err := o.Validate() @@ -32,34 +59,34 @@ func TestValidateBootstrapOptions(t *testing.T) { } } } - -// TODO: set up for complete BootstrapOptions instead of just prefix. -func TestCompleteBootstrapOptions(t *testing.T) { - completeTests := []struct { - name string - prefix string - wantPrefix string +func TestBootstrapCommandWithMissingParams(t *testing.T) { + cmdTests := []struct { + args []string + wantErr string }{ - {"no prefix", "", ""}, - {"prefix with hyphen", "test-", "test-"}, - {"prefix without hyphen", "test", "test-"}, + {[]string{"quay-username", "example", "github-token", "abc123", "dockerconfigjson", "~/"}, `Required flag(s) "git-repository" have/has not been set`}, + {[]string{"quay-username", "example", "github-token", "abc123", "git-repository", "example/repo"}, `Required flag(s) "dockerconfigjson" have/has not been set`}, + {[]string{"quay-username", "example", "dockerconfigjson", "~/", "git-repository", "example/repo"}, `Required flag(s) "github-token" have/has not been set`}, + {[]string{"github-token", "abc123", "dockerconfigjson", "~/", "git-repository", "example/repo"}, `Required flag(s) "quay-username" have/has not been set`}, } - - for _, tt := range completeTests { - o := BootstrapOptions{prefix: tt.prefix} - - err := o.Complete("test", &cobra.Command{}, []string{"test", "test/repo"}) - - if err != nil { - t.Fatalf("Complete() %#v failed: ", err) - } - - if o.prefix != tt.wantPrefix { - t.Errorf("Complete() %#v prefix: got %s, want %s", tt.name, o.prefix, tt.wantPrefix) + for _, tt := range cmdTests { + _, _, err := executeCommand(NewCmdBootstrap("bootstrap", "odo pipelines bootstrap"), tt.args...) + if err.Error() != tt.wantErr { + t.Errorf("got %s, want %s", err, tt.wantErr) } } } +func executeCommand(cmd *cobra.Command, args ...string) (c *cobra.Command, output string, err error) { + buf := new(bytes.Buffer) + cmd.SetOutput(buf) + cmd.Flags().Set(args[0], args[1]) + cmd.Flags().Set(args[2], args[3]) + cmd.Flags().Set(args[4], args[5]) + c, err = cmd.ExecuteC() + return c, buf.String(), err +} + func matchError(t *testing.T, s string, e error) bool { t.Helper() if s == "" && e == nil { diff --git a/pkg/pipelines/bootstrap.go b/pkg/pipelines/bootstrap.go index c63ecfbb956..8c3288737ef 100644 --- a/pkg/pipelines/bootstrap.go +++ b/pkg/pipelines/bootstrap.go @@ -3,8 +3,8 @@ package pipelines import ( "errors" "fmt" + "io" "os" - "path" corev1 "k8s.io/api/core/v1" v1rbac "k8s.io/api/rbac/v1" @@ -37,10 +37,18 @@ var ( } ) +// BootstrapOptions is a struct that provides the optional flags +type BootstrapOptions struct { + GithubToken string + GitRepo string + Prefix string + QuayAuthFileName string + QuayUserName string +} + // Bootstrap is the main driver for getting OpenShift pipelines for GitOps // configured with a basic configuration. -func Bootstrap(quayUsername, baseRepo, prefix string) error { - +func Bootstrap(o *BootstrapOptions) error { // First, check for Tekton. We proceed only if Tekton is installed installed, err := checkTektonInstall() if err != nil { @@ -52,28 +60,30 @@ func Bootstrap(quayUsername, baseRepo, prefix string) error { outputs := make([]interface{}, 0) - // Create GitHub Secret - githubAuth, err := createGithubSecret() + githubAuth, err := createOpaqueSecret("github-auth", o.GithubToken) if err != nil { return err } outputs = append(outputs, githubAuth) // Create Docker Secret - dockerSecret, err := createDockerSecret(quayUsername) + dockerSecret, err := createDockerSecret(o.QuayAuthFileName) if err != nil { return err } outputs = append(outputs, dockerSecret) + // Create Tasks tasks := tasks.Generate(githubAuth.GetName()) for _, task := range tasks { outputs = append(outputs, task) } - eventListener := eventlisteners.Generate(baseRepo) + // Create Event Listener + eventListener := eventlisteners.Generate(o.GitRepo) outputs = append(outputs, eventListener) + // Create route route := routes.Generate() outputs = append(outputs, route) @@ -85,48 +95,20 @@ func Bootstrap(quayUsername, baseRepo, prefix string) error { outputs = append(outputs, createRoleBinding(roleBindingName, &sa, role.Kind, role.Name)) outputs = append(outputs, createRoleBinding("edit-clusterrole-binding", &sa, "ClusterRole", "edit")) - // Marshall - for _, r := range outputs { - data, err := yaml.Marshal(r) - if err != nil { - return err - } - fmt.Printf("%s---\n", data) - } - - return nil -} - -// createGithubSecret creates Github secret -func createGithubSecret() (*corev1.Secret, error) { - tokenPath, err := pathToDownloadedFile("token") - if err != nil { - return nil, fmt.Errorf("failed to generate path to file: %w", err) - } - f, err := os.Open(tokenPath) - if err != nil { - return nil, fmt.Errorf("failed to read token file %s due to %w", tokenPath, err) - } - defer f.Close() - - githubAuth, err := createOpaqueSecret("github-auth", f) - if err != nil { - return nil, err - } - - return githubAuth, nil + return marshalOutputs(os.Stdout, outputs) } // createDockerSecret creates Docker secret -func createDockerSecret(quayUsername string) (*corev1.Secret, error) { - authJSONPath, err := pathToDownloadedFile(quayUsername + "-auth.json") +func createDockerSecret(quayIOAuthFilename string) (*corev1.Secret, error) { + + authJSONPath, err := homedir.Expand(quayIOAuthFilename) if err != nil { return nil, fmt.Errorf("failed to generate path to file: %w", err) } f, err := os.Open(authJSONPath) if err != nil { - return nil, fmt.Errorf("failed to read docker file '%s' due to %w", authJSONPath, err) + return nil, fmt.Errorf("failed to read docker file '%s' : %w", authJSONPath, err) } defer f.Close() @@ -138,9 +120,6 @@ func createDockerSecret(quayUsername string) (*corev1.Secret, error) { return dockerSecret, nil } -func pathToDownloadedFile(fname string) (string, error) { - return homedir.Expand(path.Join("~/Downloads/", fname)) -} // create and invoke a Tekton Checker func checkTektonInstall() (bool, error) { @@ -150,3 +129,18 @@ func checkTektonInstall() (bool, error) { } return tektonChecker.checkInstall() } + +// marshalOutputs marshal outputs to given writer +func marshalOutputs(out io.Writer, outputs []interface{}) error { + for _, r := range outputs { + data, err := yaml.Marshal(r) + if err != nil { + return fmt.Errorf("failed to marshal data: %w", err) + } + _, err = fmt.Fprintf(out, "%s---\n", data) + if err != nil { + return fmt.Errorf("failed to write data: %w", err) + } + } + return nil +} diff --git a/pkg/pipelines/secrets.go b/pkg/pipelines/secrets.go index f20ab7b4c41..0acb24a0745 100644 --- a/pkg/pipelines/secrets.go +++ b/pkg/pipelines/secrets.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "io/ioutil" + "strings" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -11,8 +12,10 @@ import ( // createOpaqueSecret creates a Kubernetes v1/Secret with the provided name and // body, and type Opaque. -func createOpaqueSecret(name string, in io.Reader) (*corev1.Secret, error) { - return createSecret(name, "token", corev1.SecretTypeOpaque, in) +func createOpaqueSecret(name, data string) (*corev1.Secret, error) { + r := strings.NewReader(data) + + return createSecret(name, "token", corev1.SecretTypeOpaque, r) } // createDockerConfigSecret creates a Kubernetes v1/Secret with the provided name and diff --git a/pkg/pipelines/secrets_test.go b/pkg/pipelines/secrets_test.go index 21e78714a48..f8c07f68fc2 100644 --- a/pkg/pipelines/secrets_test.go +++ b/pkg/pipelines/secrets_test.go @@ -12,8 +12,8 @@ import ( ) func TestCreateOpaqueSecret(t *testing.T) { - data := []byte(`abcdefghijklmnop`) - secret, err := createOpaqueSecret("github-auth", bytes.NewReader(data)) + data := "abcdefghijklmnop" + secret, err := createOpaqueSecret("github-auth", data) if err != nil { t.Fatal(err) } @@ -28,7 +28,7 @@ func TestCreateOpaqueSecret(t *testing.T) { }, Type: corev1.SecretTypeOpaque, Data: map[string][]byte{ - "token": data, + "token": []byte(data), }, } @@ -37,9 +37,9 @@ func TestCreateOpaqueSecret(t *testing.T) { } } -func TestCreateOpaqueSecretWithErrorReading(t *testing.T) { +func TestCreateDockerConfigSecretWithErrorReading(t *testing.T) { testErr := errors.New("test failure") - _, err := createOpaqueSecret("github-auth", errorReader{testErr}) + _, err := createDockerConfigSecret("github-auth", errorReader{testErr}) if !matchError(t, "failed to read .* test failure", err) { t.Fatalf("got an unexpected error: %#v", err) }