| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539 |
- package deploy
- import (
- "context"
- "fmt"
- "os"
- "path/filepath"
- "strings"
- api "github.com/porter-dev/porter/api/client"
- "github.com/porter-dev/porter/api/types"
- "github.com/porter-dev/porter/cli/cmd/docker"
- "github.com/porter-dev/porter/internal/templater/utils"
- )
- // CreateAgent handles the creation of a new application on Porter
- type CreateAgent struct {
- Client *api.Client
- CreateOpts *CreateOpts
- }
- // CreateOpts are required options for creating a new application on Porter: the
- // "kind" (web, worker, job) and the name of the application.
- type CreateOpts struct {
- *SharedOpts
- Kind string
- ReleaseName string
- RegistryURL string
- // Suffix for the name of the image in the repository. By default the suffix is the
- // target namespace.
- RepoSuffix string
- }
- // GithubOpts are the options for linking a Github source to the app
- type GithubOpts struct {
- Branch string
- Repo string
- }
- // CreateFromGithub uses the branch/repo to link the Github source for an application.
- // This function attempts to find a matching repository in the list of linked repositories
- // on Porter. If one is found, it will use that repository as the app source.
- func (c *CreateAgent) CreateFromGithub(
- ghOpts *GithubOpts,
- overrideValues map[string]interface{},
- ) (string, error) {
- opts := c.CreateOpts
- // get all linked github repos and find matching repo
- resp, err := c.Client.ListGitInstallationIDs(
- context.Background(),
- c.CreateOpts.ProjectID,
- )
- if err != nil {
- return "", err
- }
- gitInstallations := *resp
- var gitRepoMatch int64
- for _, gitInstallationID := range gitInstallations {
- // for each git repo, search for a matching username/owner
- resp, err := c.Client.ListGitRepos(
- context.Background(),
- c.CreateOpts.ProjectID,
- gitInstallationID,
- )
- if err != nil {
- return "", err
- }
- githubRepos := *resp
- for _, githubRepo := range githubRepos {
- if githubRepo.FullName == ghOpts.Repo {
- gitRepoMatch = gitInstallationID
- break
- }
- }
- if gitRepoMatch != 0 {
- break
- }
- }
- if gitRepoMatch == 0 {
- return "", fmt.Errorf("could not find a linked github repo for %s. Make sure you have linked your Github account on the Porter dashboard.", ghOpts.Repo)
- }
- latestVersion, mergedValues, err := c.getMergedValues(overrideValues)
- if err != nil {
- return "", err
- }
- if opts.Kind == "web" || opts.Kind == "worker" {
- mergedValues["image"] = map[string]interface{}{
- "repository": "public.ecr.aws/o1j4x7p4/hello-porter",
- "tag": "latest",
- }
- } else if opts.Kind == "job" {
- mergedValues["image"] = map[string]interface{}{
- "repository": "public.ecr.aws/o1j4x7p4/hello-porter-job",
- "tag": "latest",
- }
- }
- regID, imageURL, err := c.GetImageRepoURL(opts.ReleaseName, opts.Namespace)
- if err != nil {
- return "", err
- }
- subdomain, err := c.CreateSubdomainIfRequired(mergedValues)
- if err != nil {
- return "", err
- }
- err = c.Client.DeployTemplate(
- context.Background(),
- opts.ProjectID,
- opts.ClusterID,
- opts.Namespace,
- &types.CreateReleaseRequest{
- CreateReleaseBaseRequest: &types.CreateReleaseBaseRequest{
- TemplateName: opts.Kind,
- TemplateVersion: latestVersion,
- Values: mergedValues,
- Name: opts.ReleaseName,
- },
- ImageURL: imageURL,
- GithubActionConfig: &types.CreateGitActionConfigRequest{
- GitRepo: ghOpts.Repo,
- GitBranch: ghOpts.Branch,
- ImageRepoURI: imageURL,
- DockerfilePath: opts.LocalDockerfile,
- FolderPath: ".",
- GitRepoID: uint(gitRepoMatch),
- RegistryID: regID,
- ShouldCreateWorkflow: true,
- },
- },
- )
- if err != nil {
- return "", err
- }
- return subdomain, nil
- }
- // CreateFromRegistry deploys a new application from an existing Docker repository + tag.
- func (c *CreateAgent) CreateFromRegistry(
- image string,
- overrideValues map[string]interface{},
- ) (string, error) {
- if image == "" {
- return "", fmt.Errorf("image cannot be empty")
- }
- // split image into image-path:tag format
- imageSpl := strings.Split(image, ":")
- if len(imageSpl) != 2 {
- return "", fmt.Errorf("invalid image format: must be image-path:tag format")
- }
- opts := c.CreateOpts
- latestVersion, mergedValues, err := c.getMergedValues(overrideValues)
- if err != nil {
- return "", err
- }
- mergedValues["image"] = map[string]interface{}{
- "repository": imageSpl[0],
- "tag": imageSpl[1],
- }
- subdomain, err := c.CreateSubdomainIfRequired(mergedValues)
- if err != nil {
- return "", err
- }
- err = c.Client.DeployTemplate(
- context.Background(),
- opts.ProjectID,
- opts.ClusterID,
- opts.Namespace,
- &types.CreateReleaseRequest{
- CreateReleaseBaseRequest: &types.CreateReleaseBaseRequest{
- TemplateName: opts.Kind,
- TemplateVersion: latestVersion,
- Values: mergedValues,
- Name: opts.ReleaseName,
- },
- ImageURL: imageSpl[0],
- },
- )
- if err != nil {
- return "", err
- }
- return subdomain, nil
- }
- // CreateFromDocker uses a local build context and a local Docker daemon to build a new
- // container image, and then deploys it onto Porter.
- func (c *CreateAgent) CreateFromDocker(
- overrideValues map[string]interface{},
- imageTag string,
- extraBuildConfig *types.BuildConfig,
- ) (string, error) {
- opts := c.CreateOpts
- // detect the build config
- if opts.Method != "" {
- if opts.Method == DeployBuildTypeDocker {
- if opts.LocalDockerfile == "" {
- hasDockerfile := c.HasDefaultDockerfile(opts.LocalPath)
- if !hasDockerfile {
- return "", fmt.Errorf("Dockerfile not found")
- }
- opts.LocalDockerfile = "Dockerfile"
- }
- }
- } else {
- // try to detect dockerfile, otherwise fall back to `pack`
- hasDockerfile := c.HasDefaultDockerfile(opts.LocalPath)
- if !hasDockerfile {
- opts.Method = DeployBuildTypePack
- } else {
- opts.Method = DeployBuildTypeDocker
- opts.LocalDockerfile = "Dockerfile"
- }
- }
- // overwrite with docker image repository and tag
- regID, imageURL, err := c.GetImageRepoURL(opts.ReleaseName, opts.Namespace)
- if err != nil {
- return "", err
- }
- latestVersion, mergedValues, err := c.getMergedValues(overrideValues)
- if err != nil {
- return "", err
- }
- mergedValues["image"] = map[string]interface{}{
- "repository": imageURL,
- "tag": imageTag,
- }
- // create docker agent
- agent, err := docker.NewAgentWithAuthGetter(c.Client, opts.ProjectID)
- if err != nil {
- return "", err
- }
- env, err := GetEnvForRelease(c.Client, mergedValues, opts.ProjectID, opts.ClusterID, opts.Namespace)
- if err != nil {
- env = map[string]string{}
- }
- // add additional env based on options
- for key, val := range opts.SharedOpts.AdditionalEnv {
- env[key] = val
- }
- buildAgent := &BuildAgent{
- SharedOpts: opts.SharedOpts,
- client: c.Client,
- imageRepo: imageURL,
- env: env,
- imageExists: false,
- }
- if opts.Method == DeployBuildTypeDocker {
- basePath, err := filepath.Abs(".")
- if err != nil {
- return "", err
- }
- err = buildAgent.BuildDocker(agent, basePath, opts.LocalPath, opts.LocalDockerfile, imageTag, "")
- } else {
- err = buildAgent.BuildPack(agent, opts.LocalPath, imageTag, "", extraBuildConfig)
- }
- if err != nil {
- return "", err
- }
- // create repository
- err = c.Client.CreateRepository(
- context.Background(),
- opts.ProjectID,
- regID,
- &types.CreateRegistryRepositoryRequest{
- ImageRepoURI: imageURL,
- },
- )
- if err != nil {
- return "", err
- }
- err = agent.PushImage(fmt.Sprintf("%s:%s", imageURL, imageTag))
- if err != nil {
- return "", err
- }
- subdomain, err := c.CreateSubdomainIfRequired(mergedValues)
- if err != nil {
- return "", err
- }
- err = c.Client.DeployTemplate(
- context.Background(),
- opts.ProjectID,
- opts.ClusterID,
- opts.Namespace,
- &types.CreateReleaseRequest{
- CreateReleaseBaseRequest: &types.CreateReleaseBaseRequest{
- TemplateName: opts.Kind,
- TemplateVersion: latestVersion,
- Values: mergedValues,
- Name: opts.ReleaseName,
- },
- ImageURL: imageURL,
- },
- )
- if err != nil {
- return "", err
- }
- return subdomain, nil
- }
- // HasDefaultDockerfile detects if there is a dockerfile at the path `./Dockerfile`
- func (c *CreateAgent) HasDefaultDockerfile(buildPath string) bool {
- dockerFilePath := filepath.Join(buildPath, "./Dockerfile")
- info, err := os.Stat(dockerFilePath)
- return err == nil && !os.IsNotExist(err) && !info.IsDir()
- }
- // GetImageRepoURL creates the image repository url by finding the first valid image
- // registry linked to Porter, and then generates a new name of the form:
- // `{registry}/{name}-{namespace}`
- func (c *CreateAgent) GetImageRepoURL(name, namespace string) (uint, string, error) {
- // get all image registries linked to the project
- // get the list of namespaces
- resp, err := c.Client.ListRegistries(
- context.Background(),
- c.CreateOpts.ProjectID,
- )
- registries := *resp
- if err != nil {
- return 0, "", err
- } else if len(registries) == 0 {
- return 0, "", fmt.Errorf("must have created or linked an image registry")
- }
- // get the first non-empty registry
- var imageURI string
- var regID uint
- for _, reg := range registries {
- if c.CreateOpts.RegistryURL != "" {
- if c.CreateOpts.RegistryURL == reg.URL {
- regID = reg.ID
- if c.CreateOpts.RepoSuffix != "" {
- imageURI = fmt.Sprintf("%s/%s-%s", reg.URL, name, c.CreateOpts.RepoSuffix)
- } else {
- imageURI = fmt.Sprintf("%s/%s-%s", reg.URL, name, namespace)
- }
- break
- }
- } else if reg.URL != "" {
- regID = reg.ID
- if c.CreateOpts.RepoSuffix != "" {
- imageURI = fmt.Sprintf("%s/%s-%s", reg.URL, name, c.CreateOpts.RepoSuffix)
- } else {
- imageURI = fmt.Sprintf("%s/%s-%s", reg.URL, name, namespace)
- }
- break
- }
- }
- return regID, imageURI, nil
- }
- // GetLatestTemplateVersion retrieves the latest template version for a specific
- // Porter template from the chart repository.
- func (c *CreateAgent) GetLatestTemplateVersion(templateName string) (string, error) {
- resp, err := c.Client.ListTemplates(
- context.Background(),
- &types.ListTemplatesRequest{},
- )
- if err != nil {
- return "", err
- }
- templates := *resp
- var version string
- // find the matching template name
- for _, template := range templates {
- if templateName == template.Name {
- version = template.Versions[0]
- break
- }
- }
- if version == "" {
- return "", fmt.Errorf("matching template version not found")
- }
- return version, nil
- }
- // GetLatestTemplateDefaultValues gets the default config (`values.yaml`) set for a specific
- // template.
- func (c *CreateAgent) GetLatestTemplateDefaultValues(templateName, templateVersion string) (map[string]interface{}, error) {
- chart, err := c.Client.GetTemplate(
- context.Background(),
- templateName,
- templateVersion,
- &types.GetTemplateRequest{},
- )
- if err != nil {
- return nil, err
- }
- return chart.Values, nil
- }
- func (c *CreateAgent) getMergedValues(overrideValues map[string]interface{}) (string, map[string]interface{}, error) {
- // deploy the template
- latestVersion, err := c.GetLatestTemplateVersion(c.CreateOpts.Kind)
- if err != nil {
- return "", nil, err
- }
- // get the values of the template
- values, err := c.GetLatestTemplateDefaultValues(c.CreateOpts.Kind, latestVersion)
- if err != nil {
- return "", nil, err
- }
- // merge existing values with overriding values
- mergedValues := utils.CoalesceValues(values, overrideValues)
- return latestVersion, mergedValues, err
- }
- func (c *CreateAgent) CreateSubdomainIfRequired(mergedValues map[string]interface{}) (string, error) {
- subdomain := ""
- // check for automatic subdomain creation if web kind
- if c.CreateOpts.Kind == "web" {
- // look for ingress.enabled and no custom domains set
- ingressMap, err := getNestedMap(mergedValues, "ingress")
- if err == nil {
- enabledVal, enabledExists := ingressMap["enabled"]
- customDomVal, customDomExists := ingressMap["custom_domain"]
- if enabledExists && customDomExists {
- enabled, eOK := enabledVal.(bool)
- customDomain, cOK := customDomVal.(bool)
- // in the case of ingress enabled but no custom domain, create subdomain
- if eOK && cOK && enabled && !customDomain {
- dnsRecord, err := c.Client.CreateDNSRecord(
- context.Background(),
- c.CreateOpts.ProjectID,
- c.CreateOpts.ClusterID,
- c.CreateOpts.Namespace,
- c.CreateOpts.ReleaseName,
- )
- if err != nil {
- return "", fmt.Errorf("Error creating subdomain: %s", err.Error())
- }
- subdomain = dnsRecord.ExternalURL
- if ingressVal, ok := mergedValues["ingress"]; !ok {
- mergedValues["ingress"] = map[string]interface{}{
- "porter_hosts": []string{
- subdomain,
- },
- }
- } else {
- ingressValMap := ingressVal.(map[string]interface{})
- ingressValMap["porter_hosts"] = []string{
- subdomain,
- }
- }
- }
- }
- }
- }
- return subdomain, nil
- }
|