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  }