drone/app/gitspace/orchestrator/utils/build_with_features.go

223 lines
6.4 KiB
Go

// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package utils
import (
"archive/tar"
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/harness/gitness/types"
dockerTypes "github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/rs/zerolog/log"
)
// convertOptionsToEnvVariables converts the option keys to standardised env variables using the
// devcontainer specification to ensure uniformity in the naming and casing of the env variables.
// eg: 217yat%_fg -> _YAT_FG
// Reference: https://containers.dev/implementors/features/#option-resolution
func convertOptionsToEnvVariables(str string) string {
// Replace all non-alphanumeric characters (excluding underscores) with '_'
reNonAlnum := regexp.MustCompile(`[^\w_]`)
str = reNonAlnum.ReplaceAllString(str, "_")
// Replace leading digits or underscores with a single '_'
reLeadingDigitsOrUnderscores := regexp.MustCompile(`^[\d_]+`)
str = reLeadingDigitsOrUnderscores.ReplaceAllString(str, "_")
// Convert the string to uppercase
str = strings.ToUpper(str)
return str
}
// BuildWithFeatures builds a docker image using the provided image as the base image.
// It sets some common env variables using the ARG instruction.
// For every feature, it copies the containerEnv variables using the ENV instruction and passes the resolved options
// as env variables in the RUN instruction. It further executes the install script.
func BuildWithFeatures(
ctx context.Context,
dockerClient *client.Client,
imageName string,
features []*types.ResolvedFeature,
gitspaceInstanceIdentifier string,
containerUser string,
remoteUser string,
containerUserHomeDir string,
remoteUserHomeDir string,
) (string, error) {
buildContextPath := getGitspaceInstanceDirectory(gitspaceInstanceIdentifier)
defer func() {
err := os.RemoveAll(buildContextPath)
if err != nil {
log.Ctx(ctx).Err(err).Msgf("failed to remove build context directory %s", buildContextPath)
}
}()
err := generateDockerFileWithFeatures(imageName, features, buildContextPath, containerUser,
containerUserHomeDir, remoteUser, remoteUserHomeDir)
if err != nil {
return "", err
}
buildContext, err := packBuildContextDirectory(buildContextPath)
if err != nil {
return "", err
}
newImageName := "gitspace-with-features:" + gitspaceInstanceIdentifier
buildRes, imageBuildErr := dockerClient.ImageBuild(ctx, buildContext, dockerTypes.ImageBuildOptions{
SuppressOutput: false,
Tags: []string{newImageName},
Version: dockerTypes.BuilderBuildKit,
})
defer func() {
if buildRes.Body != nil {
closeErr := buildRes.Body.Close()
if closeErr != nil {
log.Ctx(ctx).Err(closeErr).Msg("failed to close docker image build response body")
}
}
}()
if imageBuildErr != nil {
return "", imageBuildErr
}
_, err = io.Copy(io.Discard, buildRes.Body)
if err != nil {
return "", err
}
return newImageName, nil
}
func packBuildContextDirectory(path string) (io.Reader, error) {
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
err := filepath.Walk(path, func(file string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
header, err := tar.FileInfoHeader(fi, file)
if err != nil {
return err
}
header.Name, err = filepath.Rel(path, file)
if err != nil {
return err
}
if err := tw.WriteHeader(header); err != nil {
return err
}
if fi.Mode().IsRegular() {
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(tw, f)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to walk path %q: %w", path, err)
}
if err := tw.Close(); err != nil {
return nil, fmt.Errorf("failed to close tar writer: %w", err)
}
return &buf, nil
}
// generateDockerFileWithFeatures creates and saves a dockerfile inside the build context directory.
func generateDockerFileWithFeatures(
imageName string,
features []*types.ResolvedFeature,
buildContextPath string,
containerUser string,
remoteUser string,
containerUserHomeDir string,
remoteUserHomeDir string,
) error {
dockerFile := fmt.Sprintf("FROM %s\nARG %s=%s\nARG %s=%s\nARG %s=%s\nARG %s=%s\nCOPY ./devcontainer-features %s",
imageName, convertOptionsToEnvVariables("_CONTAINER_USER"), containerUser,
convertOptionsToEnvVariables("_REMOTE_USER"), remoteUser,
convertOptionsToEnvVariables("_CONTAINER_USER_HOME"), containerUserHomeDir,
convertOptionsToEnvVariables("_REMOTE_USER_HOME"), remoteUserHomeDir,
"/tmp/devcontainer-features")
for _, feature := range features {
if len(feature.DownloadedFeature.DevcontainerFeatureConfig.ContainerEnv) > 0 {
envVariables := ""
for key, value := range feature.DownloadedFeature.DevcontainerFeatureConfig.ContainerEnv {
envVariables += " " + key + "=" + value
}
dockerFile += fmt.Sprintf("\nENV%s", envVariables)
}
finalOptionsMap := make(map[string]string)
for key, value := range feature.ResolvedOptions {
finalOptionsMap[convertOptionsToEnvVariables(key)] = value
}
optionEnvVariables := ""
for key, value := range finalOptionsMap {
optionEnvVariables += " " + key + "=" + value
}
installScriptPath := filepath.Join("/tmp/devcontainer-features",
getFeatureFolderNameWithTag(feature.DownloadedFeature.FeatureFolderName, feature.DownloadedFeature.Tag),
feature.DownloadedFeature.FeatureFolderName, "install.sh")
dockerFile += fmt.Sprintf("\nRUN%s chmod +x %s && %s",
optionEnvVariables, installScriptPath, installScriptPath)
}
log.Debug().Msgf("generated dockerfile for build context %s\n%s", buildContextPath, dockerFile)
file, err := os.OpenFile(filepath.Join(buildContextPath, "Dockerfile"), os.O_CREATE|os.O_RDWR, 0666)
if err != nil {
return fmt.Errorf("failed to create Dockerfile: %w", err)
}
defer file.Close()
_, err = file.WriteString(dockerFile)
if err != nil {
return fmt.Errorf("failed to write content to Dockerfile: %w", err)
}
return nil
}