package preview import ( "context" "fmt" "os" "path/filepath" "strings" "github.com/cli/cli/git" "github.com/docker/distribution/reference" "github.com/mitchellh/mapstructure" "github.com/porter-dev/porter/api/client" "github.com/porter-dev/porter/api/types" "github.com/porter-dev/porter/cli/cmd/config" "github.com/porter-dev/porter/cli/cmd/deploy" "github.com/porter-dev/porter/cli/cmd/docker" "github.com/porter-dev/porter/internal/integrations/preview" "github.com/porter-dev/switchboard/pkg/drivers" "github.com/porter-dev/switchboard/pkg/models" ) type BuildDriver struct { source *preview.Source target *preview.Target config *preview.BuildDriverConfig lookupTable *map[string]drivers.Driver output map[string]interface{} } func NewBuildDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) { driver := &BuildDriver{ lookupTable: opts.DriverLookupTable, output: make(map[string]interface{}), } target, err := GetTarget(resource.Name, resource.Target) if err != nil { return nil, err } driver.target = target source, err := GetSource(target.Project, resource.Name, resource.Source) if err != nil { return nil, err } driver.source = source return driver, nil } func (d *BuildDriver) ShouldApply(resource *models.Resource) bool { return true } func (d *BuildDriver) Apply(resource *models.Resource) (*models.Resource, error) { buildDriverConfig, err := d.getConfig(resource) if err != nil { return nil, err } d.config = buildDriverConfig client := config.GetAPIClient() // FIXME: give tag option in config build, but override if PORTER_TAG is present tag := os.Getenv("PORTER_TAG") if tag == "" { commit, err := git.LastCommit() if err == nil { tag = commit.Sha[:7] } } // if the method is registry and a tag is defined, we use the provided tag if d.config.Build.Method == "registry" { imageSpl := strings.Split(d.config.Build.Image, ":") if len(imageSpl) == 2 { tag = imageSpl[1] } if tag == "" { tag = "latest" } } regList, err := client.ListRegistries(context.Background(), d.target.Project) if err != nil { return nil, err } var registryURL string if len(*regList) == 0 { return nil, fmt.Errorf("no registry found") } else { registryURL = (*regList)[0].URL } var repoSuffix string if repoName := os.Getenv("PORTER_REPO_NAME"); repoName != "" { if repoOwner := os.Getenv("PORTER_REPO_OWNER"); repoOwner != "" { repoSuffix = strings.ToLower(strings.ReplaceAll(fmt.Sprintf("%s-%s", repoOwner, repoName), "_", "-")) } } createAgent := &deploy.CreateAgent{ Client: client, CreateOpts: &deploy.CreateOpts{ SharedOpts: &deploy.SharedOpts{ ProjectID: d.target.Project, ClusterID: d.target.Cluster, OverrideTag: tag, Namespace: d.target.Namespace, LocalPath: d.config.Build.Context, LocalDockerfile: d.config.Build.Dockerfile, Method: deploy.DeployBuildType(d.config.Build.Method), EnvGroups: d.config.EnvGroups, UseCache: d.config.Build.UsePackCache, }, Kind: d.source.Name, ReleaseName: d.target.AppName, RegistryURL: registryURL, RepoSuffix: repoSuffix, }, } regID, imageURL, err := createAgent.GetImageRepoURL(d.target.AppName, d.target.Namespace) if err != nil { return nil, err } // create repository if it does not exist repoResp, err := client.ListRegistryRepositories(context.Background(), d.target.Project, regID) if err != nil { return nil, err } repos := *repoResp found := false for _, repo := range repos { if repo.URI == imageURL { found = true break } } if !found { err = client.CreateRepository( context.Background(), d.target.Project, regID, &types.CreateRegistryRepositoryRequest{ ImageRepoURI: imageURL, }, ) if err != nil { return nil, err } } if d.config.Build.UsePackCache { err := config.SetDockerConfig(client) if err != nil { return nil, err } } if d.config.Build.Method != "" { if d.config.Build.Method == string(deploy.DeployBuildTypeDocker) { if d.config.Build.Dockerfile == "" { hasDockerfile := createAgent.HasDefaultDockerfile(d.config.Build.Context) if !hasDockerfile { return nil, fmt.Errorf("dockerfile not found") } d.config.Build.Dockerfile = "Dockerfile" } } } else { // try to detect dockerfile, otherwise fall back to `pack` hasDockerfile := createAgent.HasDefaultDockerfile(d.config.Build.Context) if !hasDockerfile { d.config.Build.Method = string(deploy.DeployBuildTypePack) } else { d.config.Build.Method = string(deploy.DeployBuildTypeDocker) d.config.Build.Dockerfile = "Dockerfile" } } // create docker agent agent, err := docker.NewAgentWithAuthGetter(client, d.target.Project) if err != nil { return nil, err } _, mergedValues, err := createAgent.GetMergedValues(d.config.Values) if err != nil { return nil, err } env, err := deploy.GetEnvForRelease( client, mergedValues, d.target.Project, d.target.Cluster, d.target.Namespace, ) if err != nil { env = make(map[string]string) } envConfig, err := deploy.GetNestedMap(mergedValues, "container", "env") if err == nil { _, exists := envConfig["build"] if exists { buildEnv, err := deploy.GetNestedMap(mergedValues, "container", "env", "build") if err == nil { for key, val := range buildEnv { if valStr, ok := val.(string); ok { env[key] = valStr } } } } } for k, v := range d.config.Build.Env { env[k] = v } buildAgent := &deploy.BuildAgent{ SharedOpts: createAgent.CreateOpts.SharedOpts, APIClient: client, ImageRepo: imageURL, Env: env, ImageExists: false, } if d.config.Build.Method == string(deploy.DeployBuildTypeDocker) { var basePath string basePath, err = filepath.Abs(".") if err != nil { return nil, err } var currentTag string // implement caching for porter stack builds if os.Getenv("PORTER_STACK_NAME") != "" { currentTag = getCurrentImageTagIfExists(client, d.target.Project, d.target.Cluster, os.Getenv("PORTER_STACK_NAME")) } err = buildAgent.BuildDocker( agent, basePath, d.config.Build.Context, d.config.Build.Dockerfile, tag, currentTag, ) } else { var buildConfig *types.BuildConfig if d.config.Build.Builder != "" { buildConfig = &types.BuildConfig{ Builder: d.config.Build.Builder, Buildpacks: d.config.Build.Buildpacks, } } err = buildAgent.BuildPack( agent, d.config.Build.Context, tag, "", buildConfig, ) } if err != nil { return nil, err } named, _ := reference.ParseNamed(imageURL) domain := reference.Domain(named) imageRepo := reference.Path(named) d.output["registry_url"] = domain d.output["image_repo"] = imageRepo d.output["image_tag"] = tag d.output["image"] = fmt.Sprintf("%s:%s", imageURL, tag) return resource, nil } func getCurrentImageTagIfExists(client *client.Client, projectID, clusterID uint, stackName string) string { namespace := fmt.Sprintf("porter-stack-%s", stackName) release, err := client.GetRelease( context.Background(), projectID, clusterID, namespace, stackName, ) if err != nil { return "" } if release == nil { return "" } if release.Config == nil { return "" } value, ok := release.Config["global"] if !ok { return "" } globalConfig := value.(map[string]interface{}) if globalConfig == nil { return "" } value, ok = globalConfig["image"] if !ok { return "" } imageConfig := value.(map[string]interface{}) if imageConfig == nil { return "" } value, ok = imageConfig["tag"] if !ok { return "" } tag := value.(string) return tag } func (d *BuildDriver) Output() (map[string]interface{}, error) { return d.output, nil } func (d *BuildDriver) getConfig(resource *models.Resource) (*preview.BuildDriverConfig, error) { populatedConf, err := drivers.ConstructConfig(&drivers.ConstructConfigOpts{ RawConf: resource.Config, LookupTable: *d.lookupTable, Dependencies: resource.Dependencies, }) if err != nil { return nil, err } config := &preview.BuildDriverConfig{} err = mapstructure.Decode(populatedConf, config) if err != nil { return nil, err } return config, nil }