github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/commands/alpha/rpkg/clone/command.go (about) 1 // Copyright 2022 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package clone 16 17 import ( 18 "context" 19 "fmt" 20 "strings" 21 22 "github.com/GoogleContainerTools/kpt/internal/docs/generated/rpkgdocs" 23 "github.com/GoogleContainerTools/kpt/internal/errors" 24 "github.com/GoogleContainerTools/kpt/internal/util/parse" 25 "github.com/GoogleContainerTools/kpt/internal/util/porch" 26 porchapi "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1" 27 "github.com/spf13/cobra" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/cli-runtime/pkg/genericclioptions" 30 "sigs.k8s.io/controller-runtime/pkg/client" 31 ) 32 33 const ( 34 command = "cmdrpkgclone" 35 ) 36 37 var ( 38 strategies = []string{ 39 string(porchapi.ResourceMerge), 40 string(porchapi.FastForward), 41 string(porchapi.ForceDeleteReplace), 42 } 43 ) 44 45 func NewCommand(ctx context.Context, rcg *genericclioptions.ConfigFlags) *cobra.Command { 46 return newRunner(ctx, rcg).Command 47 } 48 49 func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner { 50 r := &runner{ 51 ctx: ctx, 52 cfg: rcg, 53 } 54 c := &cobra.Command{ 55 Use: "clone SOURCE_PACKAGE NAME", 56 Short: rpkgdocs.CloneShort, 57 Long: rpkgdocs.CloneShort + "\n" + rpkgdocs.CloneLong, 58 Example: rpkgdocs.CloneExamples, 59 PreRunE: r.preRunE, 60 RunE: r.runE, 61 Hidden: porch.HidePorchCommands, 62 } 63 r.Command = c 64 65 c.Flags().StringVar(&r.strategy, "strategy", string(porchapi.ResourceMerge), 66 "update strategy that should be used when updating this package; one of: "+strings.Join(strategies, ",")) 67 c.Flags().StringVar(&r.directory, "directory", "", "Directory within the repository where the upstream package is located.") 68 c.Flags().StringVar(&r.ref, "ref", "", "Branch in the repository where the upstream package is located.") 69 c.Flags().StringVar(&r.repository, "repository", "", "Repository to which package will be cloned (downstream repository).") 70 c.Flags().StringVar(&r.revision, "revision", "v1", "Revision of the downstream package.") 71 72 return r 73 } 74 75 type runner struct { 76 ctx context.Context 77 cfg *genericclioptions.ConfigFlags 78 client client.Client 79 Command *cobra.Command 80 81 clone porchapi.PackageCloneTaskSpec 82 83 // Flags 84 strategy string 85 directory string 86 ref string 87 repository string // Target repository 88 revision string // Target package revision 89 target string // Target package name 90 } 91 92 func (r *runner) preRunE(cmd *cobra.Command, args []string) error { 93 const op errors.Op = command + ".preRunE" 94 client, err := porch.CreateClient(r.cfg) 95 if err != nil { 96 return errors.E(op, err) 97 } 98 r.client = client 99 100 mergeStrategy, err := toMergeStrategy(r.strategy) 101 if err != nil { 102 return errors.E(op, err) 103 } 104 r.clone.Strategy = mergeStrategy 105 106 if len(args) < 2 { 107 return errors.E(op, fmt.Errorf("SOURCE_PACKAGE and NAME are required positional arguments; %d provided", len(args))) 108 } 109 110 if r.repository == "" { 111 return errors.E(op, fmt.Errorf("--repository is required to specify downstream repository")) 112 } 113 114 source := args[0] 115 target := args[1] 116 117 switch { 118 case strings.HasPrefix(source, "oci://"): 119 r.clone.Upstream.Type = porchapi.RepositoryTypeOCI 120 r.clone.Upstream.Oci = &porchapi.OciPackage{ 121 Image: source, 122 } 123 124 case strings.Contains(source, "/"): 125 if parse.HasGitSuffix(source) { // extra parsing required 126 repo, dir, ref, err := parse.URL(source) 127 if err != nil { 128 return err 129 } 130 // throw error if values set by flags contradict values parsed from SOURCE_PACKAGE 131 if r.directory != "" && dir != "" && r.directory != dir { 132 return errors.E(op, fmt.Errorf("directory %s specified by --directory contradicts directory %s specified by SOURCE_PACKAGE", 133 r.directory, dir)) 134 } 135 if r.ref != "" && ref != "" && r.ref != ref { 136 return errors.E(op, fmt.Errorf("ref %s specified by --ref contradicts ref %s specified by SOURCE_PACKAGE", 137 r.ref, ref)) 138 } 139 // grab the values parsed from SOURCE_PACKAGE 140 if r.directory == "" { 141 r.directory = dir 142 } 143 if r.ref == "" { 144 r.ref = ref 145 } 146 source = repo + ".git" // parse.ParseURL removes the git suffix, we need to add it back 147 } 148 if r.ref == "" { 149 r.ref = "main" 150 } 151 if r.directory == "" { 152 r.directory = "/" 153 } 154 r.clone.Upstream.Type = porchapi.RepositoryTypeGit 155 r.clone.Upstream.Git = &porchapi.GitPackage{ 156 Repo: source, 157 Ref: r.ref, 158 Directory: r.directory, 159 } 160 // TODO: support authn 161 162 default: 163 r.clone.Upstream.UpstreamRef = &porchapi.PackageRevisionRef{ 164 Name: source, 165 } 166 } 167 168 r.target = target 169 return nil 170 } 171 172 func (r *runner) runE(cmd *cobra.Command, args []string) error { 173 const op errors.Op = command + ".runE" 174 175 pr := &porchapi.PackageRevision{ 176 TypeMeta: metav1.TypeMeta{ 177 Kind: "PackageRevision", 178 APIVersion: porchapi.SchemeGroupVersion.Identifier(), 179 }, 180 ObjectMeta: metav1.ObjectMeta{ 181 Namespace: *r.cfg.Namespace, 182 }, 183 Spec: porchapi.PackageRevisionSpec{ 184 PackageName: r.target, 185 Revision: r.revision, 186 RepositoryName: r.repository, 187 Tasks: []porchapi.Task{ 188 { 189 Type: porchapi.TaskTypeClone, 190 Clone: &r.clone, 191 }, 192 }, 193 }, 194 } 195 if err := r.client.Create(r.ctx, pr); err != nil { 196 return errors.E(op, err) 197 } 198 199 fmt.Fprintf(cmd.OutOrStdout(), "%s created\n", pr.Name) 200 return nil 201 } 202 203 func toMergeStrategy(strategy string) (porchapi.PackageMergeStrategy, error) { 204 switch strategy { 205 case string(porchapi.ResourceMerge): 206 return porchapi.ResourceMerge, nil 207 case string(porchapi.FastForward): 208 return porchapi.FastForward, nil 209 case string(porchapi.ForceDeleteReplace): 210 return porchapi.ForceDeleteReplace, nil 211 default: 212 return "", fmt.Errorf("invalid strategy: %q", strategy) 213 } 214 }