mirror of https://github.com/harness/drone.git
feat: [CDE-572]: Adding support for features- sorting and building. (#3248)
* Addressing review comments. * feat: [CDE-572]: Adding support for features- sorting and building.BT-10437
parent
362e64fd62
commit
b1d0c945d1
|
@ -15,14 +15,28 @@
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"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
|
// 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.
|
// 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
|
// Reference: https://containers.dev/implementors/features/#option-resolution
|
||||||
func ConvertOptionsToEnvVariables(str string) string {
|
func convertOptionsToEnvVariables(str string) string {
|
||||||
// Replace all non-alphanumeric characters (excluding underscores) with '_'
|
// Replace all non-alphanumeric characters (excluding underscores) with '_'
|
||||||
reNonAlnum := regexp.MustCompile(`[^\w_]`)
|
reNonAlnum := regexp.MustCompile(`[^\w_]`)
|
||||||
str = reNonAlnum.ReplaceAllString(str, "_")
|
str = reNonAlnum.ReplaceAllString(str, "_")
|
||||||
|
@ -36,3 +50,173 @@ func ConvertOptionsToEnvVariables(str string) string {
|
||||||
|
|
||||||
return 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
|
||||||
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/harness/gitness/types"
|
"github.com/harness/gitness/types"
|
||||||
"github.com/harness/gitness/types/enum"
|
"github.com/harness/gitness/types/enum"
|
||||||
|
@ -67,6 +68,10 @@ func DownloadFeatures(
|
||||||
downloadQueue <- featureSource{sourceURL: key, sourceType: value.SourceType}
|
downloadQueue <- featureSource{sourceURL: key, sourceType: value.SourceType}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Add ctx based cancellations to the below goroutines.
|
||||||
|
|
||||||
|
// NOTE: The following logic might see performance issues with spikes in memory and CPU usage.
|
||||||
|
// If there are such issues, we can introduce throttling on the basis of memory, CPU, etc.
|
||||||
go func() {
|
go func() {
|
||||||
for source := range downloadQueue {
|
for source := range downloadQueue {
|
||||||
startCh <- 1
|
startCh <- 1
|
||||||
|
@ -102,6 +107,8 @@ waitLoop:
|
||||||
default:
|
default:
|
||||||
if totalEnd > 0 && totalStart == totalEnd {
|
if totalEnd > 0 && totalStart == totalEnd {
|
||||||
break waitLoop
|
break waitLoop
|
||||||
|
} else {
|
||||||
|
time.Sleep(time.Millisecond * 10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -183,6 +190,7 @@ func downloadFeature(
|
||||||
|
|
||||||
downloadedFeature := types.DownloadedFeature{
|
downloadedFeature := types.DownloadedFeature{
|
||||||
FeatureFolderName: featureFolderName,
|
FeatureFolderName: featureFolderName,
|
||||||
|
Source: source.sourceURL,
|
||||||
SourceWithoutTag: sourceWithoutTag,
|
SourceWithoutTag: sourceWithoutTag,
|
||||||
Tag: featureTag,
|
Tag: featureTag,
|
||||||
CanonicalName: canonicalName,
|
CanonicalName: canonicalName,
|
||||||
|
@ -219,10 +227,13 @@ func getDevcontainerFeatureConfig(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error unpacking tarball for feature %s: %w", source.sourceURL, err)
|
return nil, fmt.Errorf("error unpacking tarball for feature %s: %w", source.sourceURL, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete the tarball to avoid unnecessary packaging and copying during docker build.
|
||||||
err = os.Remove(filepath.Join(downloadDirectory, tarballName))
|
err = os.Remove(filepath.Join(downloadDirectory, tarballName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error deleting tarball for feature %s: %w", source.sourceURL, err)
|
return nil, fmt.Errorf("error deleting tarball for feature %s: %w", source.sourceURL, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
devcontainerFeatureRaw, err := os.ReadFile(filepath.Join(dst, "devcontainer-feature.json"))
|
devcontainerFeatureRaw, err := os.ReadFile(filepath.Join(dst, "devcontainer-feature.json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error reading devcontainer-feature.json file for feature %s: %w",
|
return nil, fmt.Errorf("error reading devcontainer-feature.json file for feature %s: %w",
|
||||||
|
@ -251,9 +262,6 @@ func getFeatureDownloadDirectory(gitspaceInstanceIdentifier, featureFolderName,
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFeatureFolderNameWithTag(featureFolderName string, featureTag string) string {
|
func getFeatureFolderNameWithTag(featureFolderName string, featureTag string) string {
|
||||||
if featureTag == "" {
|
|
||||||
return featureFolderName
|
|
||||||
}
|
|
||||||
return featureFolderName + "-" + featureTag
|
return featureFolderName + "-" + featureTag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,208 @@
|
||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type node struct {
|
||||||
|
Digest string
|
||||||
|
Dependencies map[string]bool
|
||||||
|
Priority int
|
||||||
|
Feature *types.ResolvedFeature
|
||||||
|
}
|
||||||
|
|
||||||
|
type featureWithPriority struct {
|
||||||
|
Priority int
|
||||||
|
Feature *types.ResolvedFeature
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortFeatures sorts the features topologically, using round priorities and feature installation order override
|
||||||
|
// if provided by the user. It also keeps track of hard dependencies and soft dependencies.
|
||||||
|
// Reference: https://containers.dev/implementors/features/#dependency-installation-order-algorithm.
|
||||||
|
func SortFeatures(
|
||||||
|
featuresToBeInstalled map[string]*types.ResolvedFeature,
|
||||||
|
overrideFeatureInstallOrder []string,
|
||||||
|
) ([]*types.ResolvedFeature, error) {
|
||||||
|
sourcesWithoutTagsMappedToDigests := getSourcesWithoutTagsMappedToDigests(featuresToBeInstalled)
|
||||||
|
|
||||||
|
adjacencyList, err := buildAdjacencyList(featuresToBeInstalled, sourcesWithoutTagsMappedToDigests,
|
||||||
|
overrideFeatureInstallOrder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sortedFeatures, err := applyTopologicalSorting(adjacencyList)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortedFeatures, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSourcesWithoutTagsMappedToDigests is used to map feature source (without tags) to their digests.
|
||||||
|
// Multiple features in the install set can have the same source but different options. All these features
|
||||||
|
// will be mapped to the same source and must be installed before any dependent features.
|
||||||
|
func getSourcesWithoutTagsMappedToDigests(featuresToBeInstalled map[string]*types.ResolvedFeature) map[string][]string {
|
||||||
|
sourcesWithoutTagsMappedToDigests := map[string][]string{}
|
||||||
|
for _, featureToBeInstalled := range featuresToBeInstalled {
|
||||||
|
sourceWithoutTag := featureToBeInstalled.DownloadedFeature.SourceWithoutTag
|
||||||
|
if _, initialized := sourcesWithoutTagsMappedToDigests[sourceWithoutTag]; !initialized {
|
||||||
|
sourcesWithoutTagsMappedToDigests[sourceWithoutTag] = []string{}
|
||||||
|
}
|
||||||
|
sourcesWithoutTagsMappedToDigests[sourceWithoutTag] =
|
||||||
|
append(sourcesWithoutTagsMappedToDigests[sourceWithoutTag], featureToBeInstalled.Digest)
|
||||||
|
}
|
||||||
|
return sourcesWithoutTagsMappedToDigests
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAdjacencyList(
|
||||||
|
featuresToBeInstalled map[string]*types.ResolvedFeature,
|
||||||
|
sourcesWithoutTagsMappedToDigests map[string][]string,
|
||||||
|
overrideFeatureInstallOrder []string,
|
||||||
|
) ([]*node, error) {
|
||||||
|
counter := 0
|
||||||
|
adjacencyList := make([]*node, 0)
|
||||||
|
for _, featureToBeInstalled := range featuresToBeInstalled {
|
||||||
|
dependencies := map[string]bool{}
|
||||||
|
|
||||||
|
err := populateHardDependencies(featureToBeInstalled, dependencies)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
populateSoftDependencies(sourcesWithoutTagsMappedToDigests, featureToBeInstalled, dependencies)
|
||||||
|
|
||||||
|
// While the default priority is 0, it can be varied by the user through the overrideFeatureInstallOrder
|
||||||
|
// in the devcontainer.json.
|
||||||
|
// Reference: https://containers.dev/implementors/features/#overrideFeatureInstallOrder.
|
||||||
|
priority := 0
|
||||||
|
index := slices.Index(overrideFeatureInstallOrder, featureToBeInstalled.DownloadedFeature.SourceWithoutTag)
|
||||||
|
if index > -1 {
|
||||||
|
priority = len(overrideFeatureInstallOrder) - index
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
|
||||||
|
graphNode := node{
|
||||||
|
Digest: featureToBeInstalled.Digest,
|
||||||
|
Dependencies: dependencies,
|
||||||
|
Priority: priority,
|
||||||
|
Feature: featureToBeInstalled,
|
||||||
|
}
|
||||||
|
|
||||||
|
adjacencyList = append(adjacencyList, &graphNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any feature mentioned by the user in the overrideFeatureInstallOrder is not present in the install set,
|
||||||
|
// fail the flow.
|
||||||
|
difference := len(overrideFeatureInstallOrder) - counter
|
||||||
|
if difference > 0 {
|
||||||
|
return nil, fmt.Errorf("overrideFeatureInstallOrder contains %d extra features", difference)
|
||||||
|
}
|
||||||
|
|
||||||
|
return adjacencyList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// populateSoftDependencies populates the digests of all the features whose source name is present in the installAfter
|
||||||
|
// property for the current feature ie which must be installed before the current feature can be installed.
|
||||||
|
// Any feature mentioned in the installAfter but not part of the install set is ignored.
|
||||||
|
func populateSoftDependencies(
|
||||||
|
sourcesWithoutTagsMappedToDigests map[string][]string,
|
||||||
|
featureToBeInstalled *types.ResolvedFeature,
|
||||||
|
dependencies map[string]bool,
|
||||||
|
) {
|
||||||
|
softDependencies := featureToBeInstalled.DownloadedFeature.DevcontainerFeatureConfig.InstallsAfter
|
||||||
|
if len(softDependencies) > 0 {
|
||||||
|
for _, softDependency := range softDependencies {
|
||||||
|
if digests, ok := sourcesWithoutTagsMappedToDigests[softDependency]; ok {
|
||||||
|
for _, digest := range digests {
|
||||||
|
if _, alreadyAdded := dependencies[digest]; !alreadyAdded {
|
||||||
|
dependencies[digest] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// populateHardDependencies populates the digests of all the features which must be installed before the current
|
||||||
|
// feature can be installed.
|
||||||
|
func populateHardDependencies(featureToBeInstalled *types.ResolvedFeature, dependencies map[string]bool) error {
|
||||||
|
hardDependencies := featureToBeInstalled.DownloadedFeature.DevcontainerFeatureConfig.DependsOn
|
||||||
|
if hardDependencies != nil && len(*hardDependencies) > 0 {
|
||||||
|
for _, hardDependency := range *hardDependencies {
|
||||||
|
digest, err := calculateDigest(hardDependency.Source, hardDependency.Options)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error calculating digest for %s: %w", hardDependency.Source, err)
|
||||||
|
}
|
||||||
|
dependencies[digest] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyTopologicalSorting(
|
||||||
|
adjacencyList []*node,
|
||||||
|
) ([]*types.ResolvedFeature, error) {
|
||||||
|
sortedFeatures := make([]*types.ResolvedFeature, 0)
|
||||||
|
for len(sortedFeatures) < len(adjacencyList) {
|
||||||
|
maxPriority, eligibleFeatures := getFeaturesEligibleInThisRound(adjacencyList)
|
||||||
|
|
||||||
|
if len(eligibleFeatures) == 0 {
|
||||||
|
return nil, fmt.Errorf("features can not be sorted")
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedFeatures := []*types.ResolvedFeature{}
|
||||||
|
|
||||||
|
// only select those features which have the max priority, rest will be picked up in the next iteration.
|
||||||
|
for _, eligibleFeature := range eligibleFeatures {
|
||||||
|
if eligibleFeature.Priority == maxPriority {
|
||||||
|
selectedFeatures = append(selectedFeatures, eligibleFeature.Feature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.SortStableFunc(selectedFeatures, types.CompareResolvedFeature)
|
||||||
|
|
||||||
|
for _, selectedFeature := range selectedFeatures {
|
||||||
|
sortedFeatures = append(sortedFeatures, selectedFeature)
|
||||||
|
for _, vertex := range adjacencyList {
|
||||||
|
delete(vertex.Dependencies, selectedFeature.Digest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortedFeatures, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFeaturesEligibleInThisRound(adjacencyList []*node) (int, []featureWithPriority) {
|
||||||
|
maxPriorityInRound := 0
|
||||||
|
eligibleFeatures := []featureWithPriority{}
|
||||||
|
for _, vertex := range adjacencyList {
|
||||||
|
if len(vertex.Dependencies) == 0 {
|
||||||
|
eligibleFeatures = append(eligibleFeatures, featureWithPriority{
|
||||||
|
Priority: vertex.Priority,
|
||||||
|
Feature: vertex.Feature,
|
||||||
|
})
|
||||||
|
if maxPriorityInRound < vertex.Priority {
|
||||||
|
maxPriorityInRound = vertex.Priority
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maxPriorityInRound, eligibleFeatures
|
||||||
|
}
|
2
go.mod
2
go.mod
|
@ -4,6 +4,7 @@ go 1.22.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/storage v1.43.0
|
cloud.google.com/go/storage v1.43.0
|
||||||
|
github.com/Masterminds/semver/v3 v3.3.1
|
||||||
github.com/Masterminds/squirrel v1.5.4
|
github.com/Masterminds/squirrel v1.5.4
|
||||||
github.com/adrg/xdg v0.5.0
|
github.com/adrg/xdg v0.5.0
|
||||||
github.com/aws/aws-sdk-go v1.55.2
|
github.com/aws/aws-sdk-go v1.55.2
|
||||||
|
@ -98,7 +99,6 @@ require (
|
||||||
github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e // indirect
|
github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e // indirect
|
||||||
github.com/BobuSumisu/aho-corasick v1.0.3 // indirect
|
github.com/BobuSumisu/aho-corasick v1.0.3 // indirect
|
||||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.3.1 // indirect
|
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||||
github.com/antonmedv/expr v1.15.5 // indirect
|
github.com/antonmedv/expr v1.15.5 // indirect
|
||||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||||
|
|
|
@ -244,3 +244,13 @@ type Mount struct {
|
||||||
Target string `json:"target,omitempty"`
|
Target string `json:"target,omitempty"`
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Mount) UnmarshalJSON(data []byte) error {
|
||||||
|
// TODO: Add support for unmarshalling mount data from a string input
|
||||||
|
var mount Mount
|
||||||
|
err := json.Unmarshal(data, &mount)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -32,7 +32,6 @@ type DevcontainerFeatureConfig struct {
|
||||||
ContainerEnv map[string]string `json:"containerEnv,omitempty"`
|
ContainerEnv map[string]string `json:"containerEnv,omitempty"`
|
||||||
Privileged bool `json:"privileged,omitempty"`
|
Privileged bool `json:"privileged,omitempty"`
|
||||||
Init bool `json:"init,omitempty"`
|
Init bool `json:"init,omitempty"`
|
||||||
Deprecated bool `json:"deprecated,omitempty"`
|
|
||||||
CapAdd []string `json:"capAdd,omitempty"`
|
CapAdd []string `json:"capAdd,omitempty"`
|
||||||
SecurityOpt []string `json:"securityOpt,omitempty"`
|
SecurityOpt []string `json:"securityOpt,omitempty"`
|
||||||
Entrypoint string `json:"entrypoint,omitempty"`
|
Entrypoint string `json:"entrypoint,omitempty"`
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -31,12 +32,21 @@ type ResolvedFeature struct {
|
||||||
|
|
||||||
type DownloadedFeature struct {
|
type DownloadedFeature struct {
|
||||||
FeatureFolderName string `json:"feature_folder_name,omitempty"`
|
FeatureFolderName string `json:"feature_folder_name,omitempty"`
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
SourceWithoutTag string `json:"source_without_tag,omitempty"`
|
SourceWithoutTag string `json:"source_without_tag,omitempty"`
|
||||||
Tag string `json:"tag,omitempty"`
|
Tag string `json:"tag,omitempty"`
|
||||||
CanonicalName string `json:"canonical_name,omitempty"`
|
CanonicalName string `json:"canonical_name,omitempty"`
|
||||||
DevcontainerFeatureConfig *DevcontainerFeatureConfig `json:"devcontainer_feature_config,omitempty"`
|
DevcontainerFeatureConfig *DevcontainerFeatureConfig `json:"devcontainer_feature_config,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ResolvedFeature) Print() string {
|
||||||
|
options := make([]string, 0, len(r.ResolvedOptions))
|
||||||
|
for key, value := range r.ResolvedOptions {
|
||||||
|
options = append(options, fmt.Sprintf("%s=%s", key, value))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %+v", r.DownloadedFeature.Source, options)
|
||||||
|
}
|
||||||
|
|
||||||
// CompareResolvedFeature implements the following comparison rules.
|
// CompareResolvedFeature implements the following comparison rules.
|
||||||
// 1. Compare and sort each Feature lexicographically by their fully qualified resource name
|
// 1. Compare and sort each Feature lexicographically by their fully qualified resource name
|
||||||
// (For OCI-published Features, that means the ID without version or digest.). If the comparison is equal:
|
// (For OCI-published Features, that means the ID without version or digest.). If the comparison is equal:
|
||||||
|
|
Loading…
Reference in New Issue