mirror of https://github.com/harness/drone.git
138 lines
4.5 KiB
Go
138 lines
4.5 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 types
|
||
|
||
import (
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/Masterminds/semver/v3"
|
||
)
|
||
|
||
type ResolvedFeature struct {
|
||
ResolvedOptions map[string]string `json:"options,omitempty"`
|
||
OverriddenOptions map[string]any `json:"overridden_options,omitempty"`
|
||
Digest string `json:"string,omitempty"`
|
||
DownloadedFeature *DownloadedFeature `json:"downloaded_feature,omitempty"`
|
||
}
|
||
|
||
type DownloadedFeature struct {
|
||
FeatureFolderName string `json:"feature_folder_name,omitempty"`
|
||
SourceWithoutTag string `json:"source_without_tag,omitempty"`
|
||
Tag string `json:"tag,omitempty"`
|
||
CanonicalName string `json:"canonical_name,omitempty"`
|
||
DevcontainerFeatureConfig *DevcontainerFeatureConfig `json:"devcontainer_feature_config,omitempty"`
|
||
}
|
||
|
||
// CompareResolvedFeature implements the following comparison rules.
|
||
// 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:
|
||
// 2. Compare and sort each Feature from oldest to newest tag (latest being the “most new”). If the comparison is equal:
|
||
// 3. Compare and sort each Feature by their options by:
|
||
// 3.1 Greatest number of user-defined options (note omitting an option will default that value to the Feature’s
|
||
// default value and is not considered a user-defined option). If the comparison is equal:
|
||
// 3.2 Sort the provided option keys lexicographically. If the comparison is equal:
|
||
// 3.3 Sort the provided option values lexicographically. If the comparison is equal:
|
||
// 4. Sort Features by their canonical name (For OCI-published Features, the Feature ID resolved to the digest hash).
|
||
// 5. If there is no difference based on these comparator rules, the Features are considered equal.
|
||
// Reference: https://containers.dev/implementors/features/#definition-feature-equality (Round Stable Sort).
|
||
func CompareResolvedFeature(a, b *ResolvedFeature) int {
|
||
var comparison int
|
||
|
||
comparison = strings.Compare(a.DownloadedFeature.SourceWithoutTag, b.DownloadedFeature.SourceWithoutTag)
|
||
if comparison != 0 {
|
||
return comparison
|
||
}
|
||
|
||
comparison, _ = compareTags(a.DownloadedFeature.Tag, b.DownloadedFeature.Tag)
|
||
if comparison != 0 {
|
||
return comparison
|
||
}
|
||
|
||
comparison = compareOverriddenOptions(a.OverriddenOptions, b.OverriddenOptions)
|
||
if comparison != 0 {
|
||
return comparison
|
||
}
|
||
|
||
return strings.Compare(a.DownloadedFeature.CanonicalName, b.DownloadedFeature.CanonicalName)
|
||
}
|
||
|
||
func compareTags(a, b string) (int, error) {
|
||
if a == FeatureDefaultTag && b == FeatureDefaultTag {
|
||
return 0, nil
|
||
}
|
||
if a == FeatureDefaultTag {
|
||
return 1, nil
|
||
}
|
||
if b == FeatureDefaultTag {
|
||
return -1, nil
|
||
}
|
||
versionA, err := semver.NewVersion(a)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
versionB, err := semver.NewVersion(b)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return versionA.Compare(versionB), nil
|
||
}
|
||
|
||
func compareOverriddenOptions(a, b map[string]any) int {
|
||
if len(a) != len(b) {
|
||
return len(a) - len(b)
|
||
}
|
||
|
||
keysA, valuesA := getSortedOptions(a)
|
||
keysB, valuesB := getSortedOptions(b)
|
||
|
||
for i := 0; i < len(keysA); i++ {
|
||
if keysA[i] == keysB[i] {
|
||
continue
|
||
}
|
||
return strings.Compare(keysA[i], keysB[i])
|
||
}
|
||
|
||
for i := 0; i < len(valuesA); i++ {
|
||
if valuesA[i] == valuesB[i] {
|
||
continue
|
||
}
|
||
return strings.Compare(valuesA[i], valuesB[i])
|
||
}
|
||
|
||
return 0
|
||
}
|
||
|
||
// getSortedOptions returns the keys and values of a map sorted lexicographically.
|
||
func getSortedOptions(m map[string]any) ([]string, []string) {
|
||
keys := make([]string, 0, len(m))
|
||
for key := range m {
|
||
keys = append(keys, key)
|
||
}
|
||
sort.Strings(keys)
|
||
values := make([]string, 0, len(m))
|
||
for _, key := range keys {
|
||
value := m[key]
|
||
switch v := value.(type) {
|
||
case string:
|
||
values = append(values, v)
|
||
case bool:
|
||
values = append(values, strconv.FormatBool(v))
|
||
}
|
||
}
|
||
return keys, values
|
||
}
|