github.com/SamarSidharth/kpt@v0.0.0-20231122062228-c7d747ae3ace/commands/alpha/rpkg/clone/command.go (about) 1 // Copyright 2022 The kpt Authors 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/commands/alpha/rpkg/util" 23 "github.com/GoogleContainerTools/kpt/internal/docs/generated/rpkgdocs" 24 "github.com/GoogleContainerTools/kpt/internal/errors" 25 "github.com/GoogleContainerTools/kpt/internal/util/parse" 26 "github.com/GoogleContainerTools/kpt/internal/util/porch" 27 porchapi "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1" 28 "github.com/spf13/cobra" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/cli-runtime/pkg/genericclioptions" 31 "sigs.k8s.io/controller-runtime/pkg/client" 32 ) 33 34 const ( 35 command = "cmdrpkgclone" 36 ) 37 38 var ( 39 strategies = []string{ 40 string(porchapi.ResourceMerge), 41 string(porchapi.FastForward), 42 string(porchapi.ForceDeleteReplace), 43 } 44 ) 45 46 func NewCommand(ctx context.Context, rcg *genericclioptions.ConfigFlags) *cobra.Command { 47 return newRunner(ctx, rcg).Command 48 } 49 50 func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner { 51 r := &runner{ 52 ctx: ctx, 53 cfg: rcg, 54 } 55 c := &cobra.Command{ 56 Use: "clone SOURCE_PACKAGE NAME", 57 Short: rpkgdocs.CloneShort, 58 Long: rpkgdocs.CloneShort + "\n" + rpkgdocs.CloneLong, 59 Example: rpkgdocs.CloneExamples, 60 PreRunE: r.preRunE, 61 RunE: r.runE, 62 Hidden: porch.HidePorchCommands, 63 } 64 r.Command = c 65 66 c.Flags().StringVar(&r.strategy, "strategy", string(porchapi.ResourceMerge), 67 "update strategy that should be used when updating this package; one of: "+strings.Join(strategies, ",")) 68 c.Flags().StringVar(&r.directory, "directory", "", "Directory within the repository where the upstream package is located.") 69 c.Flags().StringVar(&r.ref, "ref", "", "Branch in the repository where the upstream package is located.") 70 c.Flags().StringVar(&r.repository, "repository", "", "Repository to which package will be cloned (downstream repository).") 71 c.Flags().StringVar(&r.workspace, "workspace", "v1", "Workspace name of the downstream package.") 72 73 return r 74 } 75 76 type runner struct { 77 ctx context.Context 78 cfg *genericclioptions.ConfigFlags 79 client client.Client 80 Command *cobra.Command 81 82 clone porchapi.PackageCloneTaskSpec 83 84 // Flags 85 strategy string 86 directory string 87 ref string 88 repository string // Target repository 89 workspace string // Target workspaceName 90 target string // Target package name 91 } 92 93 func (r *runner) preRunE(_ *cobra.Command, args []string) error { 94 const op errors.Op = command + ".preRunE" 95 client, err := porch.CreateClientWithFlags(r.cfg) 96 if err != nil { 97 return errors.E(op, err) 98 } 99 r.client = client 100 101 mergeStrategy, err := toMergeStrategy(r.strategy) 102 if err != nil { 103 return errors.E(op, err) 104 } 105 r.clone.Strategy = mergeStrategy 106 107 if len(args) < 2 { 108 return errors.E(op, fmt.Errorf("SOURCE_PACKAGE and NAME are required positional arguments; %d provided", len(args))) 109 } 110 111 if r.repository == "" { 112 return errors.E(op, fmt.Errorf("--repository is required to specify downstream repository")) 113 } 114 115 if r.workspace == "" { 116 return errors.E(op, fmt.Errorf("--workspace is required to specify downstream workspace name")) 117 } 118 119 source := args[0] 120 target := args[1] 121 122 pkgExists, err := util.PackageAlreadyExists(r.ctx, r.client, r.repository, target, *r.cfg.Namespace) 123 if err != nil { 124 return err 125 } 126 if pkgExists { 127 return fmt.Errorf("`clone` cannot create a new revision for package %q that already exists in repo %q; make subsequent revisions using `copy`", 128 target, r.repository) 129 } 130 131 switch { 132 case strings.HasPrefix(source, "oci://"): 133 r.clone.Upstream.Type = porchapi.RepositoryTypeOCI 134 r.clone.Upstream.Oci = &porchapi.OciPackage{ 135 Image: source, 136 } 137 138 case strings.Contains(source, "/"): 139 if parse.HasGitSuffix(source) { // extra parsing required 140 repo, dir, ref, err := parse.URL(source) 141 if err != nil { 142 return err 143 } 144 // throw error if values set by flags contradict values parsed from SOURCE_PACKAGE 145 if r.directory != "" && dir != "" && r.directory != dir { 146 return errors.E(op, fmt.Errorf("directory %s specified by --directory contradicts directory %s specified by SOURCE_PACKAGE", 147 r.directory, dir)) 148 } 149 if r.ref != "" && ref != "" && r.ref != ref { 150 return errors.E(op, fmt.Errorf("ref %s specified by --ref contradicts ref %s specified by SOURCE_PACKAGE", 151 r.ref, ref)) 152 } 153 // grab the values parsed from SOURCE_PACKAGE 154 if r.directory == "" { 155 r.directory = dir 156 } 157 if r.ref == "" { 158 r.ref = ref 159 } 160 source = repo + ".git" // parse.ParseURL removes the git suffix, we need to add it back 161 } 162 if r.ref == "" { 163 r.ref = "main" 164 } 165 if r.directory == "" { 166 r.directory = "/" 167 } 168 r.clone.Upstream.Type = porchapi.RepositoryTypeGit 169 r.clone.Upstream.Git = &porchapi.GitPackage{ 170 Repo: source, 171 Ref: r.ref, 172 Directory: r.directory, 173 } 174 // TODO: support authn 175 176 default: 177 r.clone.Upstream.UpstreamRef = &porchapi.PackageRevisionRef{ 178 Name: source, 179 } 180 } 181 182 r.target = target 183 return nil 184 } 185 186 func (r *runner) runE(cmd *cobra.Command, _ []string) error { 187 const op errors.Op = command + ".runE" 188 189 pr := &porchapi.PackageRevision{ 190 TypeMeta: metav1.TypeMeta{ 191 Kind: "PackageRevision", 192 APIVersion: porchapi.SchemeGroupVersion.Identifier(), 193 }, 194 ObjectMeta: metav1.ObjectMeta{ 195 Namespace: *r.cfg.Namespace, 196 }, 197 Spec: porchapi.PackageRevisionSpec{ 198 PackageName: r.target, 199 WorkspaceName: porchapi.WorkspaceName(r.workspace), 200 RepositoryName: r.repository, 201 Tasks: []porchapi.Task{ 202 { 203 Type: porchapi.TaskTypeClone, 204 Clone: &r.clone, 205 }, 206 }, 207 }, 208 } 209 if err := r.client.Create(r.ctx, pr); err != nil { 210 return errors.E(op, err) 211 } 212 213 fmt.Fprintf(cmd.OutOrStdout(), "%s created\n", pr.Name) 214 return nil 215 } 216 217 func toMergeStrategy(strategy string) (porchapi.PackageMergeStrategy, error) { 218 switch strategy { 219 case string(porchapi.ResourceMerge): 220 return porchapi.ResourceMerge, nil 221 case string(porchapi.FastForward): 222 return porchapi.FastForward, nil 223 case string(porchapi.ForceDeleteReplace): 224 return porchapi.ForceDeleteReplace, nil 225 default: 226 return "", fmt.Errorf("invalid strategy: %q", strategy) 227 } 228 }