github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/commands/alpha/rpkg/copy/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 copy 16 17 import ( 18 "context" 19 "fmt" 20 "strconv" 21 "strings" 22 23 "github.com/GoogleContainerTools/kpt/internal/docs/generated/rpkgdocs" 24 "github.com/GoogleContainerTools/kpt/internal/errors" 25 "github.com/GoogleContainerTools/kpt/internal/util/porch" 26 porchapi "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1" 27 "github.com/spf13/cobra" 28 "golang.org/x/mod/semver" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/types" 31 "k8s.io/cli-runtime/pkg/genericclioptions" 32 "sigs.k8s.io/controller-runtime/pkg/client" 33 ) 34 35 const ( 36 command = "cmdrpkgcopy" 37 ) 38 39 func NewCommand(ctx context.Context, rcg *genericclioptions.ConfigFlags) *cobra.Command { 40 return newRunner(ctx, rcg).Command 41 } 42 43 func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner { 44 r := &runner{ 45 ctx: ctx, 46 cfg: rcg, 47 } 48 r.Command = &cobra.Command{ 49 Use: "copy SOURCE_PACKAGE NAME", 50 Aliases: []string{"edit"}, 51 Short: rpkgdocs.CopyShort, 52 Long: rpkgdocs.CopyShort + "\n" + rpkgdocs.CopyLong, 53 Example: rpkgdocs.CopyExamples, 54 PreRunE: r.preRunE, 55 RunE: r.runE, 56 Hidden: porch.HidePorchCommands, 57 } 58 r.Command.Flags().StringVar(&r.revision, "revision", "", "Revision of the copied package.") 59 return r 60 } 61 62 type runner struct { 63 ctx context.Context 64 cfg *genericclioptions.ConfigFlags 65 client client.Client 66 Command *cobra.Command 67 68 copy porchapi.PackageEditTaskSpec 69 70 revision string // Target package revision 71 } 72 73 func (r *runner) preRunE(cmd *cobra.Command, args []string) error { 74 const op errors.Op = command + ".preRunE" 75 client, err := porch.CreateClient(r.cfg) 76 if err != nil { 77 return errors.E(op, err) 78 } 79 r.client = client 80 81 if len(args) < 1 { 82 return errors.E(op, fmt.Errorf("SOURCE_PACKAGE is a required positional argument")) 83 } 84 if len(args) > 1 { 85 return errors.E(op, fmt.Errorf("too many arguments; SOURCE_PACKAGE is the only accepted positional arguments")) 86 } 87 88 r.copy.Source = &porchapi.PackageRevisionRef{ 89 Name: args[0], 90 } 91 return nil 92 } 93 94 func (r *runner) runE(cmd *cobra.Command, args []string) error { 95 const op errors.Op = command + ".runE" 96 97 revisionSpec, err := r.getPackageRevisionSpec() 98 if err != nil { 99 return errors.E(op, err) 100 } 101 102 pr := &porchapi.PackageRevision{ 103 TypeMeta: metav1.TypeMeta{ 104 Kind: "PackageRevision", 105 APIVersion: porchapi.SchemeGroupVersion.Identifier(), 106 }, 107 ObjectMeta: metav1.ObjectMeta{ 108 Namespace: *r.cfg.Namespace, 109 }, 110 Spec: *revisionSpec, 111 } 112 if err := r.client.Create(r.ctx, pr); err != nil { 113 return errors.E(op, err) 114 } 115 fmt.Fprintf(cmd.OutOrStdout(), "%s created\n", pr.Name) 116 return nil 117 } 118 119 func (r *runner) getPackageRevisionSpec() (*porchapi.PackageRevisionSpec, error) { 120 packageRevision := porchapi.PackageRevision{} 121 err := r.client.Get(r.ctx, types.NamespacedName{ 122 Name: r.copy.Source.Name, 123 Namespace: *r.cfg.Namespace, 124 }, &packageRevision) 125 if err != nil { 126 return nil, err 127 } 128 129 if r.revision == "" { 130 var err error 131 r.revision, err = r.defaultPackageRevision( 132 packageRevision.Spec.PackageName, 133 packageRevision.Spec.RepositoryName, 134 ) 135 if err != nil { 136 return nil, err 137 } 138 } 139 140 spec := &porchapi.PackageRevisionSpec{ 141 PackageName: packageRevision.Spec.PackageName, 142 Revision: r.revision, 143 RepositoryName: packageRevision.Spec.RepositoryName, 144 } 145 spec.Tasks = packageRevision.Spec.Tasks 146 return spec, nil 147 } 148 149 // defaultPackageRevision attempts to return a default package revision number 150 // of "latest + 1" given a package name, repository, and namespace. 151 func (r *runner) defaultPackageRevision(packageName, repository string) (string, error) { 152 // get all package revisions 153 packageRevisionList := porchapi.PackageRevisionList{} 154 err := r.client.List(r.ctx, &packageRevisionList, client.InNamespace(*r.cfg.Namespace)) 155 if err != nil { 156 return "", err 157 } 158 159 var latestRevision string 160 allRevisions := make(map[string]bool) // this is a map for quick access 161 162 for _, rev := range packageRevisionList.Items { 163 if packageName != rev.Spec.PackageName || 164 repository != rev.Spec.RepositoryName { 165 continue 166 } 167 168 if latest, ok := rev.Labels[porchapi.LatestPackageRevisionKey]; ok { 169 if latest == porchapi.LatestPackageRevisionValue { 170 latestRevision = rev.Spec.Revision 171 } 172 } 173 allRevisions[rev.Spec.Revision] = true 174 } 175 if latestRevision == "" { 176 return "", fmt.Errorf("no published packages exist; explicit --revision flag is required") 177 } 178 179 next, err := nextRevisionNumber(latestRevision) 180 if err != nil { 181 return "", err 182 } 183 if _, ok := allRevisions[next]; ok { 184 return "", fmt.Errorf("default revision %q already exists; explicit --revision flag is required", next) 185 } 186 return next, err 187 } 188 189 func nextRevisionNumber(latestRevision string) (string, error) { 190 if !semver.IsValid(latestRevision) { 191 return "", fmt.Errorf("invalid revision format %s; explicit --revision flag is required", latestRevision) 192 } 193 194 parts := strings.Split(latestRevision[1:], ".") 195 lastIndex := len(parts) - 1 196 i, err := strconv.Atoi(parts[lastIndex]) 197 if err != nil { 198 return "", err 199 } 200 i++ 201 parts[lastIndex] = strconv.Itoa(i) 202 next := "v" + strings.Join(parts, ".") 203 return next, nil 204 }