github.com/fnproject/cli@v0.0.0-20240508150455-e5d88bd86117/commands/deploy.go (about) 1 /* 2 * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package commands 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/base64" 23 "encoding/json" 24 "errors" 25 "fmt" 26 "os" 27 "os/exec" 28 "path/filepath" 29 "regexp" 30 "strings" 31 "time" 32 33 "github.com/fnproject/fn_go/provider/oracle" 34 35 client "github.com/fnproject/cli/client" 36 common "github.com/fnproject/cli/common" 37 apps "github.com/fnproject/cli/objects/app" 38 function "github.com/fnproject/cli/objects/fn" 39 trigger "github.com/fnproject/cli/objects/trigger" 40 v2Client "github.com/fnproject/fn_go/clientv2" 41 models "github.com/fnproject/fn_go/modelsv2" 42 "github.com/oracle/oci-go-sdk/v65/artifacts" 43 ociCommon "github.com/oracle/oci-go-sdk/v65/common" 44 "github.com/oracle/oci-go-sdk/v65/keymanagement" 45 "github.com/urfave/cli" 46 ) 47 48 // Message defines the struct of container image signature payload 49 type Message struct { 50 Description string `mandatory:"true" json:"description"` 51 ImageDigest string `mandatory:"true" json:"imageDigest"` 52 KmsKeyId string `mandatory:"true" json:"kmsKeyId"` 53 KmsKeyVersionId string `mandatory:"true" json:"kmsKeyVersionId"` 54 Metadata string `mandatory:"true" json:"metadata"` 55 Region string `mandatory:"true" json:"region"` 56 RepositoryName string `mandatory:"true" json:"repositoryName"` 57 SigningAlgorithm string `mandatory:"true" json:"signingAlgorithm"` 58 } 59 60 var RegionsWithOldKMSEndpoints = map[ociCommon.Region]struct{}{ 61 ociCommon.RegionPHX: {}, 62 ociCommon.RegionIAD: {}, 63 ociCommon.RegionFRA: {}, 64 ociCommon.RegionLHR: {}, 65 ociCommon.RegionCAToronto1: {}, 66 ociCommon.RegionAPSeoul1: {}, 67 ociCommon.RegionAPTokyo1: {}, 68 ociCommon.RegionAPMumbai1: {}, 69 ociCommon.RegionEUZurich1: {}, 70 ociCommon.RegionSASaopaulo1: {}, 71 ociCommon.RegionAPSydney1: {}, 72 ociCommon.RegionMEJeddah1: {}, 73 ociCommon.RegionEUAmsterdam1: {}, 74 ociCommon.RegionAPMelbourne1: {}, 75 ociCommon.RegionAPOsaka1: {}, 76 ociCommon.RegionCAMontreal1: {}, 77 ociCommon.RegionUSLangley1: {}, 78 ociCommon.RegionUSLuke1: {}, 79 ociCommon.RegionUSGovAshburn1: {}, 80 ociCommon.RegionUSGovChicago1: {}, 81 ociCommon.RegionUSGovPhoenix1: {}, 82 ociCommon.RegionUKGovLondon1: {}, 83 } 84 85 // DeployCommand returns deploy cli.command 86 func DeployCommand() cli.Command { 87 cmd := deploycmd{} 88 var flags []cli.Flag 89 flags = append(flags, cmd.flags()...) 90 return cli.Command{ 91 Name: "deploy", 92 Usage: "\tDeploys a function to the functions server (bumps, build, pushes and updates functions and/or triggers).", 93 Aliases: []string{"dp"}, 94 Before: func(cxt *cli.Context) error { 95 provider, err := client.CurrentProvider() 96 if err != nil { 97 return err 98 } 99 cmd.clientV2 = provider.APIClientv2() 100 return nil 101 }, 102 Category: "DEVELOPMENT COMMANDS", 103 Description: "This command deploys one or all (--all) functions to the function server.", 104 ArgsUsage: "[function-subdirectory]", 105 Flags: flags, 106 Action: cmd.deploy, 107 } 108 } 109 110 type deploycmd struct { 111 clientV2 *v2Client.Fn 112 113 appName string 114 createApp bool 115 wd string 116 local bool 117 noCache bool 118 registry string 119 all bool 120 noBump bool 121 } 122 123 func (p *deploycmd) flags() []cli.Flag { 124 return []cli.Flag{ 125 cli.StringFlag{ 126 Name: "app", 127 Usage: "App name to deploy to", 128 Destination: &p.appName, 129 }, 130 cli.BoolFlag{ 131 Name: "create-app", 132 Usage: "Enable automatic creation of app if it doesn't exist during deploy", 133 Destination: &p.createApp, 134 }, 135 cli.BoolFlag{ 136 Name: "verbose, v", 137 Usage: "Verbose mode", 138 Destination: &common.CommandVerbose, 139 }, 140 cli.BoolFlag{ 141 Name: "no-cache", 142 Usage: "Don't use Docker cache for the build", 143 Destination: &p.noCache, 144 }, 145 cli.BoolFlag{ 146 Name: "local, skip-push", // todo: deprecate skip-push 147 Usage: "Do not push Docker built images onto Docker Hub - useful for local development.", 148 Destination: &p.local, 149 }, 150 cli.StringFlag{ 151 Name: "registry", 152 Usage: "Set the Docker owner for images and optionally the registry. This will be prefixed to your function name for pushing to Docker registries.\r eg: `--registry username` will set your Docker Hub owner. `--registry registry.hub.docker.com/username` will set the registry and owner. ", 153 Destination: &p.registry, 154 }, 155 cli.BoolFlag{ 156 Name: "all", 157 Usage: "If in root directory containing `app.yaml`, this will deploy all functions", 158 Destination: &p.all, 159 }, 160 cli.BoolFlag{ 161 Name: "no-bump", 162 Usage: "Do not bump the version, assuming external version management", 163 Destination: &p.noBump, 164 }, 165 cli.StringSliceFlag{ 166 Name: "build-arg", 167 Usage: "Set build time variables", 168 }, 169 cli.StringFlag{ 170 Name: "working-dir,w", 171 Usage: "Specify the working directory to deploy a function, must be the full path.", 172 }, 173 } 174 } 175 176 // deploy deploys a function or a set of functions for an app 177 // By default this will deploy a single function, either the function in the current directory 178 // or if an arg is passed in, a function in the path representing that arg, relative to the 179 // current working directory. 180 // 181 // If user passes in --all flag, it will deploy all functions in an app. An app must have an `app.yaml` 182 // file in it's root directory. The functions will be deployed based on the directory structure 183 // on the file system (can be overridden using the `path` arg in each `func.yaml`. The index/root function 184 // is the one that lives in the same directory as the app.yaml. 185 func (p *deploycmd) deploy(c *cli.Context) error { 186 187 appName := "" 188 dir := common.GetDir(c) 189 190 appf, err := common.LoadAppfile(dir) 191 if err != nil { 192 if _, ok := err.(*common.NotFoundError); ok { 193 if p.all { 194 return err 195 } 196 // otherwise, it's ok 197 } else { 198 return err 199 } 200 } else { 201 appName = appf.Name 202 } 203 if p.appName != "" { 204 // flag overrides all 205 appName = p.appName 206 } 207 208 if appName == "" { 209 return errors.New("App name must be provided, try `--app APP_NAME`") 210 } 211 212 // appfApp is used to create/update app, with app file additions if provided 213 appfApp := models.App{ 214 Name: appName, 215 } 216 if appf != nil { 217 // set other fields from app file 218 appfApp.Config = appf.Config 219 appfApp.Annotations = appf.Annotations 220 if appf.SyslogURL != "" { 221 // TODO consistent with some other fields (config), unsetting in app.yaml doesn't unset on server. undecided policy for all fields 222 appfApp.SyslogURL = &appf.SyslogURL 223 } 224 } 225 226 // find and create/update app if required 227 app, err := apps.GetAppByName(p.clientV2, appName) 228 if _, ok := err.(apps.NameNotFoundError); ok && p.createApp { 229 app, err = apps.CreateApp(p.clientV2, &appfApp) 230 if err != nil { 231 return err 232 } 233 } else if err != nil { 234 return err 235 } else if appf != nil { 236 // app exists, but we need to update it if we have an app file 237 app, err = apps.PutApp(p.clientV2, app.ID, &appfApp) 238 if err != nil { 239 return fmt.Errorf("Failed to update app config: %v", err) 240 } 241 } 242 243 if app == nil { 244 panic("app should not be nil here") // tests should catch... better than panic later 245 } 246 247 // deploy functions 248 if p.all { 249 return p.deployAll(c, app) 250 } 251 return p.deploySingle(c, app) 252 } 253 254 // deploySingle deploys a single function, either the current directory or if in the context 255 // of an app and user provides relative path as the first arg, it will deploy that function. 256 func (p *deploycmd) deploySingle(c *cli.Context, app *models.App) error { 257 var dir string 258 wd := common.GetWd() 259 260 if c.String("working-dir") != "" { 261 dir = c.String("working-dir") 262 } else { 263 // if we're in the context of an app, first arg is path to the function 264 path := c.Args().First() 265 if path != "" { 266 fmt.Printf("Deploying function at: ./%s\n", path) 267 } 268 dir = filepath.Join(wd, path) 269 } 270 271 err := os.Chdir(dir) 272 if err != nil { 273 return err 274 } 275 defer os.Chdir(wd) 276 277 fpath, ff, err := common.FindAndParseFuncFileV20180708(dir) 278 if err != nil { 279 return err 280 } 281 return p.deployFuncV20180708(c, app, fpath, ff) 282 } 283 284 // deployAll deploys all functions in an app. 285 func (p *deploycmd) deployAll(c *cli.Context, app *models.App) error { 286 var dir string 287 wd := common.GetWd() 288 289 if c.String("working-dir") != "" { 290 dir = c.String("working-dir") 291 } else { 292 // if we're in the context of an app, first arg is path to the function 293 path := c.Args().First() 294 if path != "" { 295 fmt.Printf("Deploying function at: ./%s\n", path) 296 } 297 dir = filepath.Join(wd, path) 298 } 299 300 var funcFound bool 301 err := common.WalkFuncsV20180708(dir, func(path string, ff *common.FuncFileV20180708, err error) error { 302 if err != nil { // probably some issue with funcfile parsing, can decide to handle this differently if we'd like 303 return err 304 } 305 dir := filepath.Dir(path) 306 if dir != wd { 307 // change dirs 308 err = os.Chdir(dir) 309 if err != nil { 310 return err 311 } 312 } 313 p2 := strings.TrimPrefix(dir, wd) 314 if ff.Name == "" { 315 ff.Name = strings.Replace(p2, "/", "-", -1) 316 if strings.HasPrefix(ff.Name, "-") { 317 ff.Name = ff.Name[1:] 318 } 319 } 320 321 err = p.deployFuncV20180708(c, app, path, ff) 322 if err != nil { 323 return fmt.Errorf("deploy error on %s: %v", path, err) 324 } 325 326 now := time.Now() 327 os.Chtimes(path, now, now) 328 funcFound = true 329 return nil 330 }) 331 if err != nil { 332 return err 333 } 334 335 if !funcFound { 336 return errors.New("No functions found to deploy") 337 } 338 339 return nil 340 } 341 342 func (p *deploycmd) deployFuncV20180708(c *cli.Context, app *models.App, funcfilePath string, funcfile *common.FuncFileV20180708) error { 343 if funcfile.Name == "" { 344 funcfile.Name = filepath.Base(filepath.Dir(funcfilePath)) // todo: should probably make a copy of ff before changing it 345 } 346 347 oracleProvider, _ := getOracleProvider() 348 if oracleProvider != nil && oracleProvider.ImageCompartmentID != "" { 349 // If the provider is Oracle and ImageCompartmentID is present, we need to deploy image to the ImageCompartmentID. 350 // The repository name should be unique throughout a tenancy. We check if a repository exists in the compartment and create it if it doesn't already exist. 351 // If the creation fails, it could be because the repository name aready exists in a different compartment. 352 353 repositoryName, err := getRepositoryName(funcfile) 354 if err != nil { 355 return err 356 } 357 358 artifactsClient, err := artifacts.NewArtifactsClientWithConfigurationProvider(oracleProvider.ConfigurationProvider) 359 if err != nil { 360 return err 361 } 362 artifactsClient.SetRegion(getRegion(oracleProvider)) 363 364 repositoryExists, err := doesRepositoryExistInCompartment(repositoryName, oracleProvider.ImageCompartmentID, artifactsClient) 365 if err != nil { 366 return err 367 } 368 if !repositoryExists { 369 err = createContainerRepositoryInCompartment(repositoryName, oracleProvider.ImageCompartmentID, artifactsClient) 370 if err != nil { 371 return err 372 } 373 } 374 } 375 376 fmt.Printf("Deploying %s to app: %s\n", funcfile.Name, app.Name) 377 if !p.noBump { 378 funcfile2, err := common.BumpItV20180708(funcfilePath, common.Patch) 379 if err != nil { 380 return err 381 } 382 funcfile.Version = funcfile2.Version 383 // TODO: this whole funcfile handling needs some love, way too confusing. Only bump makes permanent changes to it. 384 } 385 386 buildArgs := c.StringSlice("build-arg") 387 388 // In case of local ignore the architectures parameter 389 shape := "" 390 if !p.local { 391 // fetch the architectures 392 shape = app.Shape 393 if shape == "" { 394 shape = common.DefaultAppShape 395 app.Shape = shape 396 } 397 398 if _, ok := common.ShapeMap[shape]; !ok { 399 return errors.New(fmt.Sprintf("Invalid application : %s shape: %s", app.Name, shape)) 400 } 401 } 402 403 _, err := common.BuildFuncV20180708(common.IsVerbose(), funcfilePath, funcfile, buildArgs, p.noCache, shape) 404 if err != nil { 405 return err 406 } 407 408 if err := p.signImage(funcfile); err != nil { 409 return err 410 } 411 return p.updateFunction(c, app.ID, funcfile) 412 } 413 414 func (p *deploycmd) updateFunction(c *cli.Context, appID string, ff *common.FuncFileV20180708) error { 415 fmt.Printf("Updating function %s using image %s...\n", ff.Name, ff.ImageNameV20180708()) 416 417 fn := &models.Fn{} 418 if err := function.WithFuncFileV20180708(ff, fn); err != nil { 419 return fmt.Errorf("Error getting function with funcfile: %s", err) 420 } 421 422 fnRes, err := function.GetFnByName(p.clientV2, appID, ff.Name) 423 if _, ok := err.(function.NameNotFoundError); ok { 424 fn.Name = ff.Name 425 fn, err = function.CreateFn(p.clientV2, appID, fn) 426 if err != nil { 427 return err 428 } 429 } else if err != nil { 430 // probably service is down or something... 431 return err 432 } else { 433 fn.ID = fnRes.ID 434 err = function.PutFn(p.clientV2, fn.ID, fn) 435 if err != nil { 436 return err 437 } 438 } 439 440 if len(ff.Triggers) != 0 { 441 for _, t := range ff.Triggers { 442 trig := &models.Trigger{ 443 AppID: appID, 444 FnID: fn.ID, 445 Name: t.Name, 446 Source: t.Source, 447 Type: t.Type, 448 } 449 450 trigs, err := trigger.GetTriggerByName(p.clientV2, appID, fn.ID, t.Name) 451 if _, ok := err.(trigger.NameNotFoundError); ok { 452 err = trigger.CreateTrigger(p.clientV2, trig) 453 if err != nil { 454 return err 455 } 456 } else if err != nil { 457 return err 458 } else { 459 trig.ID = trigs.ID 460 err = trigger.PutTrigger(p.clientV2, trig) 461 if err != nil { 462 return err 463 } 464 } 465 } 466 } 467 return nil 468 } 469 470 func getOracleProvider() (*oracle.OracleProvider, error) { 471 currentProvider, err := client.CurrentProvider() 472 if err != nil { 473 return nil, err 474 } 475 if oracleProvider, ok := currentProvider.(*oracle.OracleProvider); ok { 476 return oracleProvider, nil 477 } 478 return nil, nil 479 } 480 481 func (p *deploycmd) signImage(funcfile *common.FuncFileV20180708) error { 482 signingDetails := funcfile.SigningDetails 483 signatureConfigured, err := isSignatureConfigured(signingDetails) 484 if err != nil { 485 return err 486 } 487 if !signatureConfigured { 488 return nil 489 } 490 oracleProvider, _ := getOracleProvider() 491 if oracleProvider == nil { 492 return nil 493 } 494 fmt.Printf("Signing image %s using KmsKey %s...\n", funcfile.ImageNameV20180708(), signingDetails.KmsKeyId) 495 imageDigest, err := getImageDigest(funcfile) 496 if err != nil { 497 return err 498 } 499 fmt.Printf("Image digest is %s\n", imageDigest) 500 repositoryName, err := getRepositoryName(funcfile) 501 if err != nil { 502 return err 503 } 504 fmt.Printf("Image belongs to repository %s\n", repositoryName) 505 artifactsClient, err := artifacts.NewArtifactsClientWithConfigurationProvider(oracleProvider.ConfigurationProvider) 506 if err != nil { 507 return err 508 } 509 region := getRegion(oracleProvider) 510 artifactsClient.SetRegion(region) 511 imageId, compartmentId, err := getImageId(artifactsClient, "", signingDetails.ImageCompartmentId, repositoryName, imageDigest) 512 if err != nil { 513 return err 514 } 515 signatureRequired, err := isSignatureRequired(artifactsClient, imageId, signingDetails) 516 if err != nil { 517 return err 518 } 519 if !signatureRequired { 520 fmt.Printf("Image %s is already signed by %s\n", funcfile.ImageNameV20180708(), signingDetails.KmsKeyId) 521 return nil 522 } 523 message, signature, err := createImageSignature(oracleProvider, region, imageDigest, repositoryName, funcfile.SigningDetails) 524 if err != nil { 525 return err 526 } 527 if err = uploadImageSignature(artifactsClient, compartmentId, imageId, message, signature, funcfile.SigningDetails); err == nil { 528 fmt.Printf("Successfully signed and uploaded image signature for %s\n", funcfile.ImageNameV20180708()) 529 } 530 return err 531 } 532 533 func isSignatureConfigured(signingDetails common.SigningDetails) (bool, error) { 534 configured := signingDetails.SigningAlgorithm != "" && signingDetails.KmsKeyId != "" && 535 signingDetails.ImageCompartmentId != "" && signingDetails.KmsKeyVersionId != "" 536 if !configured && (signingDetails.SigningAlgorithm != "" || signingDetails.KmsKeyId != "" || 537 signingDetails.ImageCompartmentId != "" || signingDetails.KmsKeyVersionId != "") { 538 return false, fmt.Errorf("signing_details is missing values for [%s] in func.yaml", findMissingValues(signingDetails)) 539 } 540 return configured, nil 541 } 542 543 func getRegion(oracleProvider *oracle.OracleProvider) string { 544 // try to derive region from FnApiUrl 545 if oracleProvider.FnApiUrl != nil { 546 parts := strings.Split(oracleProvider.FnApiUrl.Host, ".") 547 if len(parts) >= 4 { 548 return parts[1] 549 } 550 } 551 // provider was built after all validations, so it is safe to ignore 552 region, _ := oracleProvider.ConfigurationProvider.Region() 553 return region 554 } 555 556 func getRepositoryName(ff *common.FuncFileV20180708) (string, error) { 557 parts := strings.Split(ff.ImageNameV20180708(), ":") 558 if len(parts) != 2 { 559 return "", fmt.Errorf("cannot parse image %s", ff.ImageNameV20180708()) 560 } 561 pattern := regexp.MustCompile("(.*)ocir\\.([^/]*)/([^/]*)/(.*)") 562 parts = pattern.FindStringSubmatch(parts[0]) 563 if len(parts) != 5 { 564 return "", fmt.Errorf("cannot parse registry for image %s", ff.ImageNameV20180708()) 565 } 566 return parts[4], nil 567 } 568 569 func getImageDigest(ff *common.FuncFileV20180708) (string, error) { 570 containerEngineType, err := common.GetContainerEngineType() 571 if err != nil { 572 return "", err 573 } 574 fmt.Printf("Fetching image digest for %s\n", ff.ImageNameV20180708()) 575 parts := strings.Split(ff.ImageNameV20180708(), ":") 576 if len(parts) < 2 { 577 return "", fmt.Errorf("failed to parse image %s", ff.ImageNameV20180708()) 578 } 579 image, tag := parts[0], parts[1] 580 imageDigests, err := exec.Command(containerEngineType, "images", "--digests", image, "--format", "{{.Tag}} {{.Digest}}").Output() 581 if err != nil { 582 return "", fmt.Errorf("error while listing image digests for %s, %s", ff.ImageNameV20180708(), err) 583 } 584 cmd := exec.Command("awk", fmt.Sprintf("{if ($1==\"%s\") print $2}", tag)) 585 cmd.Stdin = bytes.NewBuffer(imageDigests) 586 output, err := cmd.Output() 587 if err != nil { 588 return "", fmt.Errorf("error parsing image digest output for %s, %s", ff.ImageNameV20180708(), err) 589 } 590 imageDigest := strings.ReplaceAll(string(output), "\n", "") 591 if imageDigest == "" { 592 return "", fmt.Errorf("failed to fetch image digest for %s", ff.ImageNameV20180708()) 593 } 594 return imageDigest, nil 595 } 596 597 func getImageId(client artifacts.ArtifactsClient, page, imageCompartmentId, repositoryName, imageDigest string) (string, string, error) { 598 request := artifacts.ListContainerImagesRequest{ 599 CompartmentId: ociCommon.String(imageCompartmentId), 600 CompartmentIdInSubtree: ociCommon.Bool(true), 601 RepositoryName: ociCommon.String(repositoryName), 602 } 603 if page != "" { 604 request.Page = ociCommon.String(page) 605 } 606 images, err := client.ListContainerImages(context.Background(), request) 607 if err != nil { 608 return "", "", fmt.Errorf("failed to lookup image in OCI Registry due to %s", err) 609 } 610 for _, image := range images.Items { 611 if image.Digest != nil && *image.Digest == imageDigest { 612 return *image.Id, *image.CompartmentId, nil 613 } 614 } 615 if images.OpcNextPage != nil { 616 return getImageId(client, *images.OpcNextPage, imageCompartmentId, repositoryName, imageDigest) 617 } 618 return "", "", fmt.Errorf("failed to fetch image details for %s from OCI Container Registry", repositoryName) 619 } 620 621 func isSignatureRequired(client artifacts.ArtifactsClient, imageId string, signingDetails common.SigningDetails) (bool, error) { 622 algorithmEnum := artifacts.ListContainerImageSignaturesSigningAlgorithmEnum(signingDetails.SigningAlgorithm) 623 signatures, err := client.ListContainerImageSignatures(context.Background(), artifacts.ListContainerImageSignaturesRequest{ 624 CompartmentId: ociCommon.String(signingDetails.ImageCompartmentId), 625 CompartmentIdInSubtree: ociCommon.Bool(true), 626 ImageId: ociCommon.String(imageId), 627 KmsKeyId: ociCommon.String(signingDetails.KmsKeyId), 628 KmsKeyVersionId: ociCommon.String(signingDetails.KmsKeyVersionId), 629 SigningAlgorithm: algorithmEnum, 630 Limit: ociCommon.Int(1), 631 }) 632 if err != nil { 633 return true, err 634 } 635 return len(signatures.Items) == 0, nil 636 } 637 638 func createImageSignature(provider *oracle.OracleProvider, region string, imageDigest string, repositoryName string, signingDetails common.SigningDetails) (string, string, error) { 639 encoded, err := createImageSignatureMessage(region, imageDigest, repositoryName, signingDetails) 640 if err != nil { 641 return "", "", nil 642 } 643 algorithm := keymanagement.SignDataDetailsSigningAlgorithmEnum(signingDetails.SigningAlgorithm) 644 cryptoEndpoint, err := buildCryptoEndpoint(region, signingDetails.KmsKeyId) 645 if err != nil { 646 return "", "", fmt.Errorf("failed to build crypto endpoint due to %s", err) 647 } 648 kmsClient, err := keymanagement.NewKmsCryptoClientWithConfigurationProvider(provider.ConfigurationProvider, cryptoEndpoint) 649 if err != nil { 650 return "", "", fmt.Errorf("failed to create crypto client due to %s", err) 651 } 652 signResponse, err := kmsClient.Sign(context.Background(), keymanagement.SignRequest{ 653 SignDataDetails: keymanagement.SignDataDetails{ 654 Message: ociCommon.String(encoded), 655 KeyId: ociCommon.String(signingDetails.KmsKeyId), 656 KeyVersionId: ociCommon.String(signingDetails.KmsKeyVersionId), 657 SigningAlgorithm: algorithm, 658 MessageType: keymanagement.SignDataDetailsMessageTypeRaw, 659 }, 660 }) 661 if err != nil { 662 return "", "", fmt.Errorf("failed to sign image due to %s", err) 663 } 664 return encoded, *signResponse.Signature, nil 665 } 666 667 func createImageSignatureMessage(region string, imageDigest string, repositoryName string, signingDetails common.SigningDetails) (string, error) { 668 message := Message{ 669 Description: "image signed by fn CLI", 670 ImageDigest: imageDigest, 671 KmsKeyId: signingDetails.KmsKeyId, 672 KmsKeyVersionId: signingDetails.KmsKeyVersionId, 673 Region: region, 674 RepositoryName: repositoryName, 675 SigningAlgorithm: signingDetails.SigningAlgorithm, 676 Metadata: "{\"signedBy\":\"fn CLI\"}", 677 } 678 messageBytes, err := json.Marshal(&message) 679 encoded := base64.StdEncoding.EncodeToString(messageBytes) 680 if err != nil { 681 return "", fmt.Errorf("failed to serialize image signature message due to %s", err) 682 } 683 return encoded, nil 684 } 685 686 func buildCryptoEndpoint(region, kmsKeyId string) (string, error) { 687 keyIdRegexp := regexp.MustCompile(`ocid1\.key\.([\w-]+)\.([\w-]+)\.([\w-]+)\.([\w]{60})`) 688 matches := keyIdRegexp.FindStringSubmatch(kmsKeyId) 689 if len(matches) != 5 { 690 return "", fmt.Errorf("keyId %s cannot be parsed", kmsKeyId) 691 } 692 vaultExt := matches[3] 693 cryptoEndpointTemplate := "https://{vaultExt}-crypto.kms.{region}.oci.{secondLevelDomain}" 694 ociRegion := ociCommon.StringToRegion(region) 695 if _, ok := RegionsWithOldKMSEndpoints[ociRegion]; ok { 696 cryptoEndpointTemplate = strings.Replace(cryptoEndpointTemplate, "oci.{secondLevelDomain}", "{secondLevelDomain}", -1) 697 } 698 cryptoEndpoint := ociRegion.EndpointForTemplate("kms", cryptoEndpointTemplate) 699 return strings.Replace(cryptoEndpoint, "{vaultExt}", vaultExt, 1), nil 700 } 701 702 func uploadImageSignature(artifactsClient artifacts.ArtifactsClient, compartmentId string, imageId string, message string, signature string, signingDetails common.SigningDetails) error { 703 algorithm := artifacts.CreateContainerImageSignatureDetailsSigningAlgorithmEnum(signingDetails.SigningAlgorithm) 704 _, err := artifactsClient.CreateContainerImageSignature(context.Background(), artifacts.CreateContainerImageSignatureRequest{ 705 CreateContainerImageSignatureDetails: artifacts.CreateContainerImageSignatureDetails{ 706 CompartmentId: ociCommon.String(compartmentId), 707 ImageId: ociCommon.String(imageId), 708 KmsKeyId: ociCommon.String(signingDetails.KmsKeyId), 709 KmsKeyVersionId: ociCommon.String(signingDetails.KmsKeyVersionId), 710 SigningAlgorithm: algorithm, 711 Message: ociCommon.String(message), 712 Signature: ociCommon.String(signature), 713 }, 714 }) 715 if err != nil { 716 return fmt.Errorf("failed to upload image signature due to %s", err) 717 } 718 return nil 719 } 720 721 func findMissingValues(signingDetails common.SigningDetails) string { 722 var missingValues []string 723 if signingDetails.ImageCompartmentId == "" { 724 missingValues = append(missingValues, "image_compartment_id") 725 } 726 if signingDetails.KmsKeyId == "" { 727 missingValues = append(missingValues, "kms_key_id") 728 } 729 if signingDetails.KmsKeyVersionId == "" { 730 missingValues = append(missingValues, "kms_key_version_id") 731 } 732 if signingDetails.SigningAlgorithm == "" { 733 missingValues = append(missingValues, "signing_algorithm") 734 } 735 return strings.Join(missingValues, ",") 736 } 737 738 // Checks if the repostitory exists in the compartment 739 func doesRepositoryExistInCompartment(repositoryName string, compartmentID string, artifactsClient artifacts.ArtifactsClient) (bool, error) { 740 response, err := artifactsClient.ListContainerRepositories(context.Background(), artifacts.ListContainerRepositoriesRequest{ 741 CompartmentId: &compartmentID, 742 DisplayName: &repositoryName}) 743 if err != nil { 744 return false, fmt.Errorf("failed to lookup container repository due to %w", err) 745 } 746 if *response.RepositoryCount == 1 { 747 return true, nil 748 } 749 return false, nil 750 } 751 752 // This function tries to create the repository in compartmentID 753 func createContainerRepositoryInCompartment(repositoryName string, compartmentID string, artifactsClient artifacts.ArtifactsClient) error { 754 _, err := artifactsClient.CreateContainerRepository(context.Background(), artifacts.CreateContainerRepositoryRequest{ 755 CreateContainerRepositoryDetails: artifacts.CreateContainerRepositoryDetails{ 756 CompartmentId: &compartmentID, 757 DisplayName: &repositoryName, 758 }, 759 }) 760 return err 761 }