Allow using imageName as a selector (#6768)

* Add integration tests highlighting our expectations

* Bump Devfile library to latest commit

f041d79870

* Expose preference that allows users to globally configure an image registry

* Return the effective Devfile view by default from the initial context

This is supposed to be read-only, so that tools can rely on it
and to the operations they need to perform right away.

Raw Devfile objects can still be obtained upon request
if there is need to update them (for example via 'odo add/remove
binding' commands.

* Pass the image registry preference to the Devfile parser to build the effective view

* Fix 'odo init' integration tests

- The test spec was actually not doing what it was supposed to do
- Now 'odo init' returns a complete Devfile, where the parent is flattened,
  because the goal of 'odo init' is to bootstrap a Devfile.
  Previously, 'odo init' would not download the parent referenced,
  making it hard to understand the resulting Devfile.

* Document how odo now handles relative image names as selectors

* fixup! Document how odo now handles relative image names as selectors

Co-authored-by: Philippe Martin <phmartin@redhat.com>

* Revert "Fix 'odo init' integration tests"

This reverts commit 78868b03fd.

Co-authored-by: Philippe Martin <phmartin@redhat.com>

* Do not make `odo init` return an effective Devfile as a result

This would change the behavior of `odo init`.

Furthermore, due to an issue [1] in the Devfile library,
it is not possible to parse some Devfiles with parents linked as GitHub URLs (like GitHub release artifacts).

[1] https://github.com/devfile/api/issues/1119

Co-authored-by: Philippe Martin <phmartin@redhat.com>

* fixup! Document how odo now handles relative image names as selectors

---------

Co-authored-by: Philippe Martin <phmartin@redhat.com>
This commit is contained in:
Armel Soro
2023-05-22 16:45:27 +02:00
committed by GitHub
parent 59e790564d
commit ec747b4ab3
72 changed files with 3527 additions and 207 deletions

View File

@@ -313,6 +313,165 @@ is invoked, that is, in this example, when the `run` or `deploy` commands are in
Because the `kubernetes` component `k8s-deploybydefault-false-and-not-referenced` has `deployByDefault` set to `false` and is not referenced by any `apply` commands, it will never be applied.
### How `odo` handles image names
When the Devfile contains an Image Component with a relative `imageName` field, `odo` treats this field as an image name selector;
it will scan the whole Devfile and automatically replace matching image names with a unique value that will be pushed to a user-defined registry.
This replacement is done in matching Container components and manifests referenced in Kubernetes/OpenShift components.
An image is said to match a relative image name if they both have the same name, regardless of their registry, tag, or digest.
At the moment, `odo` only performs replacement in the manifests of the following core Kubernetes resources:
- [CronJob](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/)
- [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/)
- [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/)
- [Job](https://kubernetes.io/docs/concepts/workloads/controllers/job/)
- [Pod](https://kubernetes.io/docs/concepts/workloads/pods/)
- [ReplicaSet](https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/)
- [ReplicationController](https://kubernetes.io/docs/concepts/workloads/controllers/replicationcontroller/)
- [StatefulSet](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/)
Replacement is done only if the user has set the `ImageRegistry` preference, which can be done via the `odo preference set ImageRegistry` command.
See the [Configuration](../overview/configure.md#configuring-global-settings) page for more details.
<details>
<summary>Example</summary>
```shell
$ odo preference set ImageRegistry quay.io/$USER
✓ Value of 'imageregistry' preference was set to 'quay.io/user'
```
</details>
In this case, `odo` will automatically build relative Image components and push them to the specified registry path defined in the Preferences,
using a dynamic image tag, so that the images built and pushed can be unique across executions of `odo`.
As an example, given the following Devfile excerpt (simplified to use only Kubernetes components, but the same behavior applies to OpenShift ones),
and provided the `ImageRegistry` preference has been set (say to `quay.io/user`):
```yaml
schemaVersion: 2.2.0
metadata:
name: my-app
version: 1.0.0
components:
- image:
imageName: "my-relative-image"
name: relative-image
- image:
imageName: "ghcr.io/myorg/my-absolute-image"
name: absolute-image
- container:
# [some-registry/]my-relative-image[:tag][@digest] will all match
# 'my-relative-image' defined in the `relative-image' component.
image: "my-relative-image"
name: cont1
- name: k8s-uri
kubernetes:
# Resources referenced via URIs will be resolved and inlined at runtime,
# so that replacement logic can also be applied on those manifests.
uri: kubernetes/k8s.yaml
- name: k8s-inlined
kubernetes:
inlined: |
apiVersion: batch/v1
kind: Job
metadata:
name: my-job
spec:
template:
metadata:
name: my-job-app
spec:
containers:
# Matches the imageName field of the 'relative-image' Image Component
# => it will be replaced dynamically regardless of the registry, tag or digest.
- image: "quay.io/my-relative-image@sha256:26c68657ccce2cb0a3"
name: my-main-cont1
initContainers:
- image: "busybox"
name: my-init-cont1
```
Because the `relative-image` Image component uses a relative image name (`my-relative-image`), it will automatically be built and pushed
by `odo` to an image named as follows: `<ImageRegistry>/<DevfileName>-<ImageName>:<SomeUniqueId>`,
where `<SomeUniqueId>` is a dynamic tag different for each execution of `odo`.
So the resulting image in the example above could be for instance: `quay.io/user/my-app-my-relative-image:1234567`
This new value will then be replaced in the following matching components and manifests:
- `cont1` container component
- the image name of `my-main-cont1` in the Job definition of the `k8s-inlined` Kubernetes component
- any matching image names in the manifests referenced by URIs in the `k8s-uri` Kubernetes component
For reference, the resulting Devfile generated by `odo` for this example would look like this:
<details>
<summary>Example</summary>
```yaml
schemaVersion: 2.2.0
metadata:
name: my-app
version: 1.0.0
components:
- image:
# highlight-next-line
imageName: "quay.io/user/my-app-my-relative-image:3295110"
name: relative-image
- image:
# Left unchanged: Absolute image names are not used as selectors.
imageName: "ghcr.io/myorg/my-absolute-image"
name: absolute-image
- container:
# highlight-next-line
image: "quay.io/user/my-app-my-relative-image:3295110"
name: cont1
- name: k8s-uri
kubernetes:
inlined: |
# highlight-start
# Resources referenced via URIs will be resolved and inlined at runtime,
# so that replacement logic can also be applied on those manifests.
# ...
# highlight-end
- name: k8s-inlined
kubernetes:
inlined: |
apiVersion: batch/v1
kind: Job
metadata:
name: my-job
spec:
template:
metadata:
name: my-job-app
spec:
containers:
# highlight-next-line
- image: "quay.io/user/my-app-my-relative-image:3295110"
name: my-main-cont1
initContainers:
# highlight-next-line
# Not replaced because it does not match 'my-relative-image'.
- image: "busybox"
name: my-init-cont1
```
</details>
## File Reference
This file reference outlines the **major** components of the Devfile API Reference using *snippets* and *examples*.

View File

@@ -64,6 +64,7 @@ Preference parameters:
PARAMETER VALUE
ConsentTelemetry true
Ephemeral true
ImageRegistry quay.io/user
PushTimeout
RegistryCacheTime
Timeout
@@ -113,15 +114,15 @@ Unsetting a preference key sets it to an empty value in the preference file. `od
### Preference Key Table
| Preference | Description | Default |
| ------------------ |--------------------------------------------------------------------------| ----------- |
| UpdateNotification | Control whether a notification to update `odo` is shown | True |
| Timeout | Timeout for Kubernetes server connection check | 1 second |
| PushTimeout | Timeout for waiting for a component to start | 240 seconds |
| RegistryCacheTime | Duration for which `odo` will cache information from the Devfile registry | 4 Minutes |
| Ephemeral | Control whether `odo` should create a emptyDir volume to store source code | False |
| ConsentTelemetry | Control whether `odo` can collect telemetry for the user's `odo` usage | False |
| Preference | Description | Default |
|--------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|
| UpdateNotification | Control whether a notification to update `odo` is shown | True |
| Timeout | Timeout for Kubernetes server connection check | 1 second |
| PushTimeout | Timeout for waiting for a component to start | 240 seconds |
| RegistryCacheTime | Duration for which `odo` will cache information from the Devfile registry | 4 Minutes |
| Ephemeral | Control whether `odo` should create a emptyDir volume to store source code | False |
| ConsentTelemetry | Control whether `odo` can collect telemetry for the user's `odo` usage | False |
| ImageRegistry | The container image registry where relative image names will be automatically pushed to. See [How `odo` handles image names](../development/devfile.md#how-odo-handles-image-names) for more details. | |
## Managing Devfile registries

5
go.mod
View File

@@ -8,8 +8,8 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.5
github.com/Xuanwo/go-locale v1.1.0
github.com/blang/semver v3.5.1+incompatible
github.com/devfile/api/v2 v2.2.0
github.com/devfile/library/v2 v2.2.1-0.20230330160000-c1b23d25e652
github.com/devfile/api/v2 v2.2.1-alpha.0.20230413012049-a6c32fca0dbd
github.com/devfile/library/v2 v2.2.1-0.20230515084048-f041d798707c
github.com/devfile/registry-support/index/generator v0.0.0-20230322155332-33914affc83b
github.com/devfile/registry-support/registry-library v0.0.0-20221201200738-19293ac0b8ab
github.com/fatih/color v1.14.1
@@ -93,6 +93,7 @@ require (
github.com/creack/pty v1.1.17 // indirect
github.com/danieljoos/wincred v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/distribution/v3 v3.0.0-20211118083504-a29a3c99a684 // indirect
github.com/docker/cli v20.10.13+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker v20.10.24+incompatible // indirect

8
go.sum
View File

@@ -368,13 +368,14 @@ github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
github.com/devfile/api/v2 v2.0.0-20211021164004-dabee4e633ed/go.mod h1:d99eTN6QxgzihOOFyOZA+VpUyD4Q1pYRYHZ/ci9J96Q=
github.com/devfile/api/v2 v2.0.0-20220117162434-6e6e6a8bc14c/go.mod h1:d99eTN6QxgzihOOFyOZA+VpUyD4Q1pYRYHZ/ci9J96Q=
github.com/devfile/api/v2 v2.2.0 h1:3Mwl/dtT508oU4pNt/v4G8vqvjoZqi9LOInXCNwKMoc=
github.com/devfile/api/v2 v2.2.0/go.mod h1:dN7xFrOVG+iPqn4UKGibXLd5oVsdE8XyK9OEb5JL3aI=
github.com/devfile/api/v2 v2.2.1-alpha.0.20230413012049-a6c32fca0dbd h1:HpGR728CfB6BB9ZuFtQb0UeTIYNFgpuGsuoMOJNMUTM=
github.com/devfile/api/v2 v2.2.1-alpha.0.20230413012049-a6c32fca0dbd/go.mod h1:qp8jcw12y1JdCsxjK/7LJ7uWaJOxcY1s2LUk5PhbkbM=
github.com/devfile/library v1.2.1-0.20211104222135-49d635cb492f/go.mod h1:uFZZdTuRqA68FVe/JoJHP92CgINyQkyWnM2Qyiim+50=
github.com/devfile/library v1.2.1-0.20220308191614-f0f7e11b17de/go.mod h1:GSPfJaBg0+bBjBHbwBE5aerJLH6tWGQu2q2rHYd9czM=
github.com/devfile/library/v2 v2.0.1/go.mod h1:paJ0PARAVy0br13VpBEQ4fO3rZVDxWtooQ29+23PNBk=
github.com/devfile/library/v2 v2.2.1-0.20230330160000-c1b23d25e652 h1:Lrg3ypN1LyGJ2yZAjU5tKh19tUPmHSH9dClyaxh6vko=
github.com/devfile/library/v2 v2.2.1-0.20230330160000-c1b23d25e652/go.mod h1:NWmKLN9RWVX5f4sCimjFtGX1REuXiYoBYU/Jcd11XAA=
github.com/devfile/library/v2 v2.2.1-0.20230515084048-f041d798707c h1:WXsJ/mP865odQ6yxP9MKULBg6N6lPmoBzMZ/Ur2y/5I=
github.com/devfile/library/v2 v2.2.1-0.20230515084048-f041d798707c/go.mod h1:7oEhkC6GW6OKmAP8HbxbaQ+nFbnACQuU7anYhJroltQ=
github.com/devfile/registry-support/index/generator v0.0.0-20220222194908-7a90a4214f3e/go.mod h1:iRPBxs+ZjfLEduVXpCCIOzdD2588Zv9OCs/CcXMcCCY=
github.com/devfile/registry-support/index/generator v0.0.0-20220527155645-8328a8a883be/go.mod h1:1fyDJL+fPHtcrYA6yjSVWeLmXmjCNth0d5Rq1rvtryc=
github.com/devfile/registry-support/index/generator v0.0.0-20221018203505-df96d34d4273/go.mod h1:ZJnaSLjTKCvGJhWmYgQoQ1O3g78qBe4Va6ZugLmi4dE=
@@ -386,7 +387,6 @@ github.com/devfile/registry-support/registry-library v0.0.0-20221201200738-19293
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/distribution/distribution v2.7.1+incompatible h1:aGFx4EvJWKEh//lHPLwFhFgwFHKH06TzNVPamrMn04M=
github.com/distribution/distribution/v3 v3.0.0-20211118083504-a29a3c99a684 h1:DBZ2sN7CK6dgvHVpQsQj4sRMCbWTmd17l+5SUCjnQSY=
github.com/distribution/distribution/v3 v3.0.0-20211118083504-a29a3c99a684/go.mod h1:UfCu3YXJJCI+IdnqGgYP82dk2+Joxmv+mUTVBES6wac=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=

View File

@@ -9,7 +9,6 @@ import (
"testing"
devfilepkg "github.com/devfile/api/v2/pkg/devfile"
"github.com/devfile/library/v2/pkg/devfile"
"github.com/devfile/library/v2/pkg/devfile/parser"
devfileCtx "github.com/devfile/library/v2/pkg/devfile/parser/context"
"github.com/devfile/library/v2/pkg/devfile/parser/data"
@@ -24,6 +23,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/redhat-developer/odo/pkg/devfile"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/labels"
"github.com/redhat-developer/odo/pkg/libdevfile"
@@ -492,8 +492,7 @@ func TestGatherName(t *testing.T) {
return nil, dir, err
}
var d parser.DevfileObj
d, _, err = devfile.ParseDevfileAndValidate(parser.ParserArgs{Path: dPath})
d, err := devfile.ParseAndValidateFromFile(dPath, "", false)
if err != nil {
return nil, dir, err
}

View File

@@ -48,7 +48,7 @@ func NewDeployClient(kubeClient kclient.ClientInterface, configAutomountClient c
func (o *DeployClient) Deploy(ctx context.Context) error {
var (
devfileObj = odocontext.GetDevfileObj(ctx)
devfileObj = odocontext.GetEffectiveDevfileObj(ctx)
devfilePath = odocontext.GetDevfilePath(ctx)
path = filepath.Dir(devfilePath)
componentName = odocontext.GetComponentName(ctx)

View File

@@ -14,7 +14,7 @@ import (
func (o *DevClient) CleanupResources(ctx context.Context, out io.Writer) error {
var (
componentName = odocontext.GetComponentName(ctx)
devfileObj = odocontext.GetDevfileObj(ctx)
devfileObj = odocontext.GetEffectiveDevfileObj(ctx)
)
fmt.Fprintln(out, "Cleaning resources, please wait")
appname := odocontext.GetApplication(ctx)

View File

@@ -106,7 +106,7 @@ func (o *DevClient) Start(
// RegenerateAdapterAndPush get the new devfile and pushes the files to remote pod
func (o *DevClient) regenerateAdapterAndPush(ctx context.Context, pushParams common.PushParameters, componentStatus *watch.ComponentStatus) error {
devObj, err := devfile.ParseAndValidateFromFileWithVariables(location.DevfileLocation(""), pushParams.StartOptions.Variables)
devObj, err := devfile.ParseAndValidateFromFileWithVariables(location.DevfileLocation(""), pushParams.StartOptions.Variables, o.prefClient.GetImageRegistry(), true)
if err != nil {
return fmt.Errorf("unable to read devfile: %w", err)
}

View File

@@ -1408,7 +1408,7 @@ func Test_createPodFromComponent(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
devfileObj := tt.args.devfileObj()
ctx = odocontext.WithDevfileObj(ctx, &devfileObj)
ctx = odocontext.WithEffectiveDevfileObj(ctx, &devfileObj)
ctx = odocontext.WithApplication(ctx, tt.args.appName)
ctx = odocontext.WithComponentName(ctx, tt.args.componentName)
ctx = odocontext.WithWorkingDirectory(ctx, "/tmp/dir")

View File

@@ -17,6 +17,7 @@ import (
odocontext "github.com/redhat-developer/odo/pkg/odo/context"
"github.com/redhat-developer/odo/pkg/podman"
"github.com/redhat-developer/odo/pkg/portForward"
"github.com/redhat-developer/odo/pkg/preference"
"github.com/redhat-developer/odo/pkg/state"
"github.com/redhat-developer/odo/pkg/sync"
"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
@@ -36,6 +37,7 @@ type DevClient struct {
fs filesystem.Filesystem
podmanClient podman.Client
prefClient preference.Client
portForwardClient portForward.Client
syncClient sync.Client
execClient exec.Client
@@ -51,6 +53,7 @@ var _ dev.Client = (*DevClient)(nil)
func NewDevClient(
fs filesystem.Filesystem,
podmanClient podman.Client,
prefClient preference.Client,
portForwardClient portForward.Client,
syncClient sync.Client,
execClient exec.Client,
@@ -60,6 +63,7 @@ func NewDevClient(
return &DevClient{
fs: fs,
podmanClient: podmanClient,
prefClient: prefClient,
portForwardClient: portForwardClient,
syncClient: syncClient,
execClient: execClient,
@@ -95,7 +99,7 @@ func (o *DevClient) Start(
// syncFiles syncs the local source files in path into the pod's source volume
func (o *DevClient) syncFiles(ctx context.Context, options dev.StartOptions, pod *corev1.Pod, path string) (bool, error) {
var (
devfileObj = odocontext.GetDevfileObj(ctx)
devfileObj = odocontext.GetEffectiveDevfileObj(ctx)
componentName = odocontext.GetComponentName(ctx)
)
@@ -161,7 +165,7 @@ func (o *DevClient) checkVolumesFree(pod *corev1.Pod) error {
func (o *DevClient) watchHandler(ctx context.Context, pushParams common.PushParameters, componentStatus *watch.ComponentStatus) error {
devObj, err := devfile.ParseAndValidateFromFileWithVariables(location.DevfileLocation(""), pushParams.StartOptions.Variables)
devObj, err := devfile.ParseAndValidateFromFileWithVariables(location.DevfileLocation(""), pushParams.StartOptions.Variables, o.prefClient.GetImageRegistry(), true)
if err != nil {
return fmt.Errorf("unable to read devfile: %w", err)
}

View File

@@ -2,8 +2,11 @@ package devfile
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/devfile/api/v2/pkg/validation/variables"
"github.com/devfile/library/v2/pkg/devfile"
"github.com/devfile/library/v2/pkg/devfile/parser"
"k8s.io/utils/pointer"
@@ -12,19 +15,96 @@ import (
"github.com/redhat-developer/odo/pkg/log"
)
func parseDevfile(args parser.ParserArgs) (parser.DevfileObj, error) {
devObj, varWarnings, err := devfile.ParseDevfileAndValidate(args)
if err != nil {
return parser.DevfileObj{}, err
}
func parseRawDevfile(args parser.ParserArgs) (parser.DevfileObj, error) {
args.FlattenedDevfile = pointer.Bool(false)
args.ConvertKubernetesContentInUri = pointer.Bool(false)
args.ImageNamesAsSelector = nil
args.SetBooleanDefaults = pointer.Bool(false)
// odo specific validations
err = validate.ValidateDevfileData(devObj.Data)
devfileObj, varWarnings, err := devfile.ParseDevfileAndValidate(args)
if err != nil {
return parser.DevfileObj{}, err
}
// display warnings related to variable substitution
displayVariableWarnings(varWarnings)
return devfileObj, nil
}
func parseEffectiveDevfile(args parser.ParserArgs) (parser.DevfileObj, error) {
// Effective Devfile with everything resolved (e.g., parent flattened, K8s URIs inlined, ...)
args.SetBooleanDefaults = pointer.Bool(false)
args.FlattenedDevfile = pointer.Bool(true)
args.ConvertKubernetesContentInUri = pointer.Bool(true)
if args.ImageNamesAsSelector != nil && args.ImageNamesAsSelector.Registry != "" {
// Tag should be a unique build identifier
args.ImageNamesAsSelector.Tag = strconv.Itoa(os.Getpid())
} else {
args.ImageNamesAsSelector = nil
}
var varWarnings variables.VariableWarning
devfileObj, varWarnings, err := devfile.ParseDevfileAndValidate(args)
if err != nil {
return parser.DevfileObj{}, err
}
// odo specific validations
err = validate.ValidateDevfileData(devfileObj.Data)
if err != nil {
return parser.DevfileObj{}, err
}
// display warnings related to variable substitution
displayVariableWarnings(varWarnings)
return devfileObj, nil
}
// ParseAndValidateFromFile reads, parses and validates devfile from a file
// if there are warning it logs them on stdout
func ParseAndValidateFromFile(devfilePath string, imageRegistry string, wantEffective bool) (parser.DevfileObj, error) {
parserArgs := parser.ParserArgs{
Path: devfilePath,
ImageNamesAsSelector: &parser.ImageSelectorArgs{
Registry: imageRegistry,
},
}
if wantEffective {
return parseEffectiveDevfile(parserArgs)
}
return parseRawDevfile(parserArgs)
}
// ParseAndValidateFromFileWithVariables reads, parses and validates devfile from a file
// variables are used to override devfile variables.
// If wantEffective is true, it returns a complete view of the Devfile, where everything is resolved.
// For example, parent will be flattened in the child, and Kubernetes manifests referenced by URI will be inlined in the related components.
// If there are warnings, it logs them on stdout.
func ParseAndValidateFromFileWithVariables(devfilePath string, variables map[string]string, imageRegistry string, wantEffective bool) (parser.DevfileObj, error) {
parserArgs := parser.ParserArgs{
Path: devfilePath,
ExternalVariables: variables,
ImageNamesAsSelector: &parser.ImageSelectorArgs{
Registry: imageRegistry,
},
}
if wantEffective {
return parseEffectiveDevfile(parserArgs)
}
return parseRawDevfile(parserArgs)
}
func displayVariableWarnings(varWarnings variables.VariableWarning) {
variableWarning := func(section string, variable string, messages []string) string {
var quotedVars []string
for _, v := range messages {
quotedVars = append(quotedVars, fmt.Sprintf("%q", v))
}
return fmt.Sprintf("Invalid variable(s) %s in %q section with name %q. ", strings.Join(quotedVars, ","), section, variable)
}
for variable, messages := range varWarnings.Commands {
log.Warningf(variableWarning("commands", variable, messages))
}
@@ -38,34 +118,4 @@ func parseDevfile(args parser.ParserArgs) (parser.DevfileObj, error) {
log.Warningf(variableWarning("starterProjects", variable, messages))
}
return devObj, nil
}
// ParseAndValidateFromFile reads, parses and validates devfile from a file
// if there are warning it logs them on stdout
func ParseAndValidateFromFile(devfilePath string) (parser.DevfileObj, error) {
return parseDevfile(parser.ParserArgs{
Path: devfilePath,
SetBooleanDefaults: pointer.Bool(false),
})
}
// ParseAndValidateFromFileWithVariables reads, parses and validates devfile from a file
// variables are used to override devfile variables
// if there are warning it logs them on stdout
func ParseAndValidateFromFileWithVariables(devfilePath string, variables map[string]string) (parser.DevfileObj, error) {
return parseDevfile(parser.ParserArgs{
Path: devfilePath,
ExternalVariables: variables,
SetBooleanDefaults: pointer.Bool(false),
})
}
func variableWarning(section string, variable string, messages []string) string {
quotedVars := []string{}
for _, v := range messages {
quotedVars = append(quotedVars, fmt.Sprintf("%q", v))
}
return fmt.Sprintf("Invalid variable(s) %s in %q section with name %q. ", strings.Join(quotedVars, ","), section, variable)
}

View File

@@ -34,7 +34,7 @@ var lookPathCmd = exec.LookPath
// If push is true, also push the images to their registries
func BuildPushImages(ctx context.Context, fs filesystem.Filesystem, push bool) error {
var (
devfileObj = odocontext.GetDevfileObj(ctx)
devfileObj = odocontext.GetEffectiveDevfileObj(ctx)
devfilePath = odocontext.GetDevfilePath(ctx)
path = filepath.Dir(devfilePath)
)

View File

@@ -10,13 +10,12 @@ import (
"strings"
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/v2/pkg/devfile"
"github.com/devfile/library/v2/pkg/devfile/parser"
dfutil "github.com/devfile/library/v2/pkg/util"
"k8s.io/utils/pointer"
"github.com/redhat-developer/odo/pkg/alizer"
"github.com/redhat-developer/odo/pkg/api"
"github.com/redhat-developer/odo/pkg/devfile"
"github.com/redhat-developer/odo/pkg/devfile/location"
"github.com/redhat-developer/odo/pkg/init/asker"
"github.com/redhat-developer/odo/pkg/init/backend"
@@ -288,11 +287,7 @@ func (o *InitClient) SelectAndPersonalizeDevfile(ctx context.Context, flags map[
return parser.DevfileObj{}, "", nil, fmt.Errorf("unable to download devfile: %w", err)
}
devfileObj, _, err := devfile.ParseDevfileAndValidate(parser.ParserArgs{
Path: devfilePath,
FlattenedDevfile: pointer.Bool(false),
SetBooleanDefaults: pointer.Bool(false),
})
devfileObj, err := devfile.ParseAndValidateFromFile(devfilePath, "", false)
if err != nil {
return parser.DevfileObj{}, "", nil, fmt.Errorf("unable to parse devfile: %w", err)
}

View File

@@ -12,11 +12,14 @@ import (
"github.com/redhat-developer/odo/pkg/binding/asker"
"github.com/redhat-developer/odo/pkg/binding/backend"
"github.com/redhat-developer/odo/pkg/devfile"
"github.com/redhat-developer/odo/pkg/devfile/location"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
odocontext "github.com/redhat-developer/odo/pkg/odo/context"
"github.com/redhat-developer/odo/pkg/odo/genericclioptions"
"github.com/redhat-developer/odo/pkg/odo/genericclioptions/clientset"
odoutil "github.com/redhat-developer/odo/pkg/util"
)
// BindingRecommendedCommandName is the recommended binding sub-command name
@@ -68,14 +71,23 @@ func (o *AddBindingOptions) Complete(ctx context.Context, cmdline cmdline.Cmdlin
}
func (o *AddBindingOptions) Validate(ctx context.Context) (err error) {
devfileObj := odocontext.GetDevfileObj(ctx)
devfileObj := odocontext.GetEffectiveDevfileObj(ctx)
withDevfile := devfileObj != nil
return o.clientset.BindingClient.ValidateAddBinding(o.flags, withDevfile)
}
func (o *AddBindingOptions) Run(ctx context.Context) error {
devfileObj := odocontext.GetDevfileObj(ctx)
withDevfile := devfileObj != nil
// Update the raw Devfile only, so we do not break any relationship between parent-child for example
withDevfile := odoutil.CheckPathExists(location.DevfileLocation(odocontext.GetWorkingDirectory(ctx)))
var devfileObj *parser.DevfileObj
if withDevfile {
rawDevfileObj, err := devfile.ParseAndValidateFromFile(odocontext.GetDevfilePath(ctx), "", false)
if err != nil {
return err
}
devfileObj = &rawDevfileObj
}
ns, err := o.clientset.BindingClient.SelectNamespace(o.flags)
if err != nil {
return err

View File

@@ -55,7 +55,7 @@ func (o *BuildImagesOptions) Complete(ctx context.Context, cmdline cmdline.Cmdli
// Validate validates the LoginOptions based on completed values
func (o *BuildImagesOptions) Validate(ctx context.Context) (err error) {
devfileObj := odocontext.GetDevfileObj(ctx)
devfileObj := odocontext.GetEffectiveDevfileObj(ctx)
if devfileObj == nil {
return genericclioptions.NewNoDevfileError(odocontext.GetWorkingDirectory(ctx))
}

View File

@@ -110,7 +110,7 @@ func (o *ComponentOptions) Complete(ctx context.Context, cmdline cmdline.Cmdline
// 1. Name is not passed, and odo has access to devfile.yaml; Name is not passed so we assume that odo has access to the devfile.yaml
if o.name == "" {
devfileObj := odocontext.GetDevfileObj(ctx)
devfileObj := odocontext.GetEffectiveDevfileObj(ctx)
if devfileObj == nil {
return genericclioptions.NewNoDevfileError(odocontext.GetWorkingDirectory(ctx))
}
@@ -248,7 +248,7 @@ func printRemainingResources(ctx context.Context, remainingResources []unstructu
// devfileObj in context must not be nil when this method is called
func (o *ComponentOptions) deleteDevfileComponent(ctx context.Context) ([]unstructured.Unstructured, error) {
var (
devfileObj = odocontext.GetDevfileObj(ctx)
devfileObj = odocontext.GetEffectiveDevfileObj(ctx)
componentName = odocontext.GetComponentName(ctx)
appName = odocontext.GetApplication(ctx)

View File

@@ -651,7 +651,7 @@ func TestComponentOptions_deleteDevfileComponent(t *testing.T) {
ctx = odocontext.WithApplication(ctx, "app")
ctx = odocontext.WithWorkingDirectory(ctx, workingDir)
ctx = odocontext.WithComponentName(ctx, compName)
ctx = odocontext.WithDevfileObj(ctx, &info)
ctx = odocontext.WithEffectiveDevfileObj(ctx, &info)
remainingResources, err := o.deleteDevfileComponent(ctx)
if (err != nil) != tt.wantErr {
t.Errorf("deleteDevfileComponent() error = %v, wantErr %v", err, tt.wantErr)

View File

@@ -3,7 +3,9 @@ package deploy
import (
"context"
"fmt"
dfutil "github.com/devfile/library/v2/pkg/util"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/component"
@@ -60,7 +62,7 @@ func (o *DeployOptions) Complete(ctx context.Context, cmdline cmdline.Cmdline, a
// Validate validates the DeployOptions based on completed values
func (o *DeployOptions) Validate(ctx context.Context) error {
devfileObj := odocontext.GetDevfileObj(ctx)
devfileObj := odocontext.GetEffectiveDevfileObj(ctx)
if devfileObj == nil {
return genericclioptions.NewNoDevfileError(odocontext.GetWorkingDirectory(ctx))
}
@@ -75,7 +77,7 @@ func (o *DeployOptions) Validate(ctx context.Context) error {
// Run contains the logic for the odo command
func (o *DeployOptions) Run(ctx context.Context) error {
var (
devfileObj = odocontext.GetDevfileObj(ctx)
devfileObj = odocontext.GetEffectiveDevfileObj(ctx)
devfileName = odocontext.GetComponentName(ctx)
namespace = odocontext.GetNamespace(ctx)
)

View File

@@ -53,7 +53,7 @@ func (o *BindingOptions) SetClientset(clientset *clientset.Clientset) {
func (o *BindingOptions) Complete(ctx context.Context, cmdline cmdline.Cmdline, args []string) (err error) {
if o.nameFlag == "" {
devfileObj := odocontext.GetDevfileObj(ctx)
devfileObj := odocontext.GetEffectiveDevfileObj(ctx)
if devfileObj == nil {
return genericclioptions.NewNoDevfileError(odocontext.GetWorkingDirectory(ctx))
}
@@ -95,7 +95,7 @@ func (o *BindingOptions) RunForJsonOutput(ctx context.Context) (out interface{},
func (o *BindingOptions) runWithoutName(ctx context.Context) ([]api.ServiceBinding, error) {
var (
workingDir = odocontext.GetWorkingDirectory(ctx)
devfileObj = odocontext.GetDevfileObj(ctx)
devfileObj = odocontext.GetEffectiveDevfileObj(ctx)
)
result, err := o.clientset.BindingClient.GetBindingsFromDevfile(*devfileObj, workingDir)

View File

@@ -71,7 +71,7 @@ func (o *ComponentOptions) Complete(ctx context.Context, cmdline cmdline.Cmdline
if platform == commonflags.PlatformCluster && len(o.namespaceFlag) > 0 {
return errors.New("--namespace can be used only with --name")
}
devfileObj := odocontext.GetDevfileObj(ctx)
devfileObj := odocontext.GetEffectiveDevfileObj(ctx)
if devfileObj == nil {
return genericclioptions.NewNoDevfileError(odocontext.GetWorkingDirectory(ctx))
}
@@ -210,7 +210,7 @@ func (o *ComponentOptions) describeNamedComponent(ctx context.Context, name stri
// describeDevfileComponent describes the component defined by the devfile in the current directory
func (o *ComponentOptions) describeDevfileComponent(ctx context.Context) (result api.Component, devfile *parser.DevfileObj, err error) {
var (
devfileObj = odocontext.GetDevfileObj(ctx)
devfileObj = odocontext.GetEffectiveDevfileObj(ctx)
devfilePath = odocontext.GetDevfilePath(ctx)
componentName = odocontext.GetComponentName(ctx)
)

View File

@@ -112,7 +112,7 @@ func (o *DevOptions) Complete(ctx context.Context, cmdline cmdline.Cmdline, args
}
func (o *DevOptions) Validate(ctx context.Context) error {
devfileObj := *odocontext.GetDevfileObj(ctx)
devfileObj := *odocontext.GetEffectiveDevfileObj(ctx)
if !o.debugFlag && !libdevfile.HasRunCommand(devfileObj.Data) {
return clierrors.NewNoCommandInDevfileError("run")
}
@@ -179,7 +179,7 @@ func (o *DevOptions) Validate(ctx context.Context) error {
func (o *DevOptions) Run(ctx context.Context) (err error) {
var (
devFileObj = odocontext.GetDevfileObj(ctx)
devFileObj = odocontext.GetEffectiveDevfileObj(ctx)
devfilePath = odocontext.GetDevfilePath(ctx)
path = filepath.Dir(devfilePath)
componentName = odocontext.GetComponentName(ctx)

View File

@@ -10,11 +10,11 @@ import (
"k8s.io/klog"
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/v2/pkg/devfile"
"github.com/devfile/library/v2/pkg/devfile/parser"
"github.com/redhat-developer/odo/pkg/api"
"github.com/redhat-developer/odo/pkg/component"
"github.com/redhat-developer/odo/pkg/devfile"
"github.com/redhat-developer/odo/pkg/devfile/location"
"github.com/redhat-developer/odo/pkg/init/backend"
"github.com/redhat-developer/odo/pkg/libdevfile"
@@ -33,7 +33,6 @@ import (
"github.com/redhat-developer/odo/pkg/version"
"k8s.io/kubectl/pkg/util/templates"
"k8s.io/utils/pointer"
)
// RecommendedCommandName is the recommended command name
@@ -236,11 +235,7 @@ func (o *InitOptions) run(ctx context.Context) (devfileObj parser.DevfileObj, pa
// in case the starter project contains a devfile, read it again
if containsDevfile {
devfileObj, _, err = devfile.ParseDevfileAndValidate(parser.ParserArgs{
Path: devfilePath,
FlattenedDevfile: pointer.Bool(false),
SetBooleanDefaults: pointer.Bool(false),
})
devfileObj, err = devfile.ParseAndValidateFromFile(devfilePath, "", false)
if err != nil {
return parser.DevfileObj{}, "", "", nil, nil, err
}

View File

@@ -89,7 +89,7 @@ func (o *BindingListOptions) RunForJsonOutput(ctx context.Context) (out interfac
func (o *BindingListOptions) run(ctx context.Context) (api.ResourcesList, error) {
var (
workingDir = odocontext.GetWorkingDirectory(ctx)
devfileObj = odocontext.GetDevfileObj(ctx)
devfileObj = odocontext.GetEffectiveDevfileObj(ctx)
)
bindings, inDevfile, err := o.clientset.BindingClient.ListAllBindings(devfileObj, workingDir)
if err != nil {

View File

@@ -105,7 +105,7 @@ func (lo *ListOptions) RunForJsonOutput(ctx context.Context) (out interface{}, e
func (lo *ListOptions) run(ctx context.Context) (api.ResourcesList, error) {
var (
devfileObj = odocontext.GetDevfileObj(ctx)
devfileObj = odocontext.GetEffectiveDevfileObj(ctx)
componentName = odocontext.GetComponentName(ctx)
kubeClient = lo.clientset.KubernetesClient

View File

@@ -113,7 +113,7 @@ func (lo *ListOptions) RunForJsonOutput(ctx context.Context) (out interface{}, e
func (lo *ListOptions) run(ctx context.Context) (list api.ResourcesList, err error) {
var (
devfileObj = odocontext.GetDevfileObj(ctx)
devfileObj = odocontext.GetEffectiveDevfileObj(ctx)
componentName = odocontext.GetComponentName(ctx)
kubeClient = lo.clientset.KubernetesClient

View File

@@ -84,7 +84,7 @@ func (o *LogsOptions) Complete(ctx context.Context, cmdline cmdline.Cmdline, _ [
return errors.New("this command cannot run in an empty directory, run the command in a directory containing source code or initialize using 'odo init'")
}
devfileObj := odocontext.GetDevfileObj(ctx)
devfileObj := odocontext.GetEffectiveDevfileObj(ctx)
if devfileObj == nil {
return genericclioptions.NewNoDevfileError(odocontext.GetWorkingDirectory(ctx))
}

View File

@@ -107,6 +107,7 @@ func HumanReadableOutput(preferenceList api.PreferenceList, registryList []api.R
}
registryT.Render()
}
func showBlankIfNil(intf interface{}) interface{} {
imm := reflect.ValueOf(intf)

View File

@@ -8,6 +8,7 @@ import (
ktemplates "k8s.io/kubectl/pkg/util/templates"
"github.com/redhat-developer/odo/pkg/binding/backend"
"github.com/redhat-developer/odo/pkg/devfile"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/odo/cmdline"
odocontext "github.com/redhat-developer/odo/pkg/odo/context"
@@ -44,7 +45,7 @@ func (o *RemoveBindingOptions) SetClientset(clientset *clientset.Clientset) {
}
func (o *RemoveBindingOptions) Complete(ctx context.Context, cmdline cmdline.Cmdline, args []string) (err error) {
devfileObj := odocontext.GetDevfileObj(ctx)
devfileObj := odocontext.GetEffectiveDevfileObj(ctx)
if devfileObj == nil {
return genericclioptions.NewNoDevfileError(odocontext.GetWorkingDirectory(ctx))
}
@@ -57,7 +58,13 @@ func (o *RemoveBindingOptions) Validate(ctx context.Context) (err error) {
}
func (o *RemoveBindingOptions) Run(ctx context.Context) error {
devfileObj := odocontext.GetDevfileObj(ctx)
// Update the raw Devfile only, so we do not break any relationship between parent-child for example
rawDevfileObj, err := devfile.ParseAndValidateFromFile(odocontext.GetDevfilePath(ctx), "", false)
if err != nil {
return err
}
devfileObj := &rawDevfileObj
newDevfileObj, err := o.clientset.BindingClient.RemoveBinding(o.flags[backend.FLAG_NAME], *devfileObj)
if err != nil {
return err

View File

@@ -7,21 +7,21 @@ import (
)
type (
applicationKeyType struct{}
cwdKeyType struct{}
pidKeyType struct{}
devfilePathKeyType struct{}
devfileObjKeyType struct{}
componentNameKeyType struct{}
applicationKeyType struct{}
cwdKeyType struct{}
pidKeyType struct{}
devfilePathKeyType struct{}
effectiveDevfileObjKeyType struct{}
componentNameKeyType struct{}
)
var (
applicationKey applicationKeyType
cwdKey cwdKeyType
pidKey pidKeyType
devfilePathKey devfilePathKeyType
devfileObjKey devfileObjKeyType
componentNameKey componentNameKeyType
applicationKey applicationKeyType
cwdKey cwdKeyType
pidKey pidKeyType
devfilePathKey devfilePathKeyType
effectiveDevfileObjKey effectiveDevfileObjKeyType
componentNameKey componentNameKeyType
)
// WithApplication sets the value of the application in ctx
@@ -94,22 +94,22 @@ func GetDevfilePath(ctx context.Context) string {
panic("this should not happen, either the original context is not passed or WithDevfilePath is not called as it should. Check that FILESYSTEM dependency is added to the command")
}
// WithDevfileObj sets the value of the devfile object in ctx
// This function must be called before using GetDevfileObj
func WithDevfileObj(ctx context.Context, val *parser.DevfileObj) context.Context {
return context.WithValue(ctx, devfileObjKey, val)
// WithEffectiveDevfileObj sets the value of the devfile object in ctx
// This function must be called before using GetEffectiveDevfileObj
func WithEffectiveDevfileObj(ctx context.Context, val *parser.DevfileObj) context.Context {
return context.WithValue(ctx, effectiveDevfileObjKey, val)
}
// GetDevfileObj gets the devfile object value in ctx
// GetEffectiveDevfileObj gets the effective Devfile object value in ctx
// This function will panic if the context does not contain the value
// Use this function only with a context obtained from Complete/Validate/Run/... methods of Runnable interface
// and only if the runnable have added the FILESYSTEM dependency to its clientset
func GetDevfileObj(ctx context.Context) *parser.DevfileObj {
value := ctx.Value(devfileObjKey)
func GetEffectiveDevfileObj(ctx context.Context) *parser.DevfileObj {
value := ctx.Value(effectiveDevfileObjKey)
if cast, ok := value.(*parser.DevfileObj); ok {
return cast
}
panic("this should not happen, either the original context is not passed or WithDevfileObj is not called as it should. Check that FILESYSTEM dependency is added to the command")
panic("this should not happen, either the original context is not passed or WithEffectiveDevfileObj is not called as it should. Check that FILESYSTEM dependency is added to the command")
}
// WithComponentName sets the name of the component in ctx

View File

@@ -280,6 +280,7 @@ func Fetch(command *cobra.Command, platform string) (*Clientset, error) {
dep.DevClient = podmandev.NewDevClient(
dep.FS,
dep.PodmanClient,
dep.PreferenceClient,
dep.PortForwardClient,
dep.SyncClient,
dep.ExecClient,

View File

@@ -5,6 +5,7 @@ import (
"github.com/devfile/library/v2/pkg/devfile/parser"
dfutil "github.com/devfile/library/v2/pkg/util"
"github.com/redhat-developer/odo/pkg/component"
"github.com/redhat-developer/odo/pkg/devfile"
"github.com/redhat-developer/odo/pkg/devfile/location"
@@ -12,7 +13,7 @@ import (
odoutil "github.com/redhat-developer/odo/pkg/util"
)
func getDevfileInfo(workingDir string, variables map[string]string) (
func getDevfileInfo(workingDir string, variables map[string]string, imageRegistry string) (
devfilePath string,
devfileObj *parser.DevfileObj,
componentName string,
@@ -27,7 +28,7 @@ func getDevfileInfo(workingDir string, variables map[string]string) (
}
// Parse devfile and validate
var devObj parser.DevfileObj
devObj, err = devfile.ParseAndValidateFromFileWithVariables(devfilePath, variables)
devObj, err = devfile.ParseAndValidateFromFileWithVariables(devfilePath, variables, imageRegistry, true)
if err != nil {
return "", nil, "", fmt.Errorf("failed to parse the devfile %s: %w", devfilePath, err)
}

View File

@@ -249,13 +249,13 @@ func GenericRun(o Runnable, cmd *cobra.Command, args []string) error {
var devfilePath, componentName string
var devfileObj *parser.DevfileObj
devfilePath, devfileObj, componentName, err = getDevfileInfo(cwd, variables)
devfilePath, devfileObj, componentName, err = getDevfileInfo(cwd, variables, userConfig.GetImageRegistry())
if err != nil {
startTelemetry(cmd, err, startTime)
return err
}
ctx = odocontext.WithDevfilePath(ctx, devfilePath)
ctx = odocontext.WithDevfileObj(ctx, devfileObj)
ctx = odocontext.WithEffectiveDevfileObj(ctx, devfileObj)
ctx = odocontext.WithComponentName(ctx, componentName)
}

View File

@@ -46,6 +46,10 @@ type odoSettings struct {
// ConsentTelemetry if true collects telemetry for odo
ConsentTelemetry *bool `yaml:"ConsentTelemetry,omitempty"`
// ImageRegistry is the image registry to which relative image names in Devfile Image Components will be pushed to.
// This will also serve as the base path for replacing matching images in other components like Container and Kubernetes/OpenShift ones.
ImageRegistry *string `yaml:"ImageRegistry,omitempty"`
}
// Registry includes the registry metadata
@@ -306,6 +310,9 @@ func (c *preferenceInfo) SetConfiguration(parameter string, value string) error
return fmt.Errorf("unable to set %q to %q, value must be a boolean", parameter, value)
}
c.OdoSettings.ConsentTelemetry = &val
case "imageregistry":
c.OdoSettings.ImageRegistry = &value
}
} else {
return fmt.Errorf("unknown parameter : %q is not a parameter in odo preference, run `odo preference -h` to see list of available parameters", parameter)
@@ -374,6 +381,12 @@ func (c *preferenceInfo) GetRegistryCacheTime() time.Duration {
return kpointer.DurationDeref(c.OdoSettings.RegistryCacheTime, DefaultRegistryCacheTime)
}
// GetImageRegistry returns the value of ImageRegistry from the preferences
// and, if absent, then returns default empty string.
func (c *preferenceInfo) GetImageRegistry() string {
return kpointer.StringDeref(c.OdoSettings.ImageRegistry, "")
}
// GetUpdateNotification returns the value of UpdateNotification from preferences
// and if absent then returns default
func (c *preferenceInfo) GetUpdateNotification() bool {

View File

@@ -57,6 +57,13 @@ func toPreferenceItems(prefInfo preferenceInfo) []api.PreferenceItem {
Type: getType(prefInfo.GetEphemeral()),
Description: EphemeralSettingDescription,
},
{
Name: ImageRegistrySetting,
Value: settings.ImageRegistry,
Default: "",
Type: getType(prefInfo.GetImageRegistry()),
Description: ImageRegistrySettingDescription,
},
}
}

View File

@@ -105,6 +105,20 @@ func (mr *MockClientMockRecorder) GetEphemeralSourceVolume() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEphemeralSourceVolume", reflect.TypeOf((*MockClient)(nil).GetEphemeralSourceVolume))
}
// GetImageRegistry mocks base method.
func (m *MockClient) GetImageRegistry() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetImageRegistry")
ret0, _ := ret[0].(string)
return ret0
}
// GetImageRegistry indicates an expected call of GetImageRegistry.
func (mr *MockClientMockRecorder) GetImageRegistry() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetImageRegistry", reflect.TypeOf((*MockClient)(nil).GetImageRegistry))
}
// GetPushTimeout mocks base method.
func (m *MockClient) GetPushTimeout() time.Duration {
m.ctrl.T.Helper()

View File

@@ -17,6 +17,7 @@ type Client interface {
GetEphemeralSourceVolume() bool
GetConsentTelemetry() bool
GetRegistryCacheTime() time.Duration
GetImageRegistry() string
RegistryHandler(operation string, registryName string, registryURL string, forceFlag bool, isSecure bool) error
UpdateNotification() *bool

View File

@@ -38,6 +38,9 @@ const (
// RegistryCacheTimeSetting is human-readable description for the registrycachetime setting
RegistryCacheTimeSetting = "RegistryCacheTime"
// ImageRegistrySetting is the name of the setting controlling ImageRegistry
ImageRegistrySetting = "ImageRegistry"
// DefaultDevfileRegistryName is the name of default devfile registry
DefaultDevfileRegistryName = "DefaultDevfileRegistry"
@@ -78,6 +81,8 @@ var EphemeralSettingDescription = fmt.Sprintf("If true, odo will create an empty
// ConsentTelemetrySettingDescription adds a description for TelemetryConsentSetting
var ConsentTelemetrySettingDescription = fmt.Sprintf("If true, odo will collect telemetry for the user's odo usage (Default: %t)\n\t\t For more information: https://developers.redhat.com/article/tool-data-collection", DefaultConsentTelemetrySetting)
const ImageRegistrySettingDescription = "Image Registry to which relative image names in Devfile Image Components will be pushed to (Example: quay.io/my-user/)"
// This value can be provided to set a seperate directory for users 'homedir' resolution
// note for mocking purpose ONLY
var customHomeDir = os.Getenv("CUSTOM_HOMEDIR")
@@ -91,6 +96,7 @@ var (
RegistryCacheTimeSetting: RegistryCacheTimeSettingDescription,
EphemeralSetting: EphemeralSettingDescription,
ConsentTelemetrySetting: ConsentTelemetrySettingDescription,
ImageRegistrySetting: ImageRegistrySettingDescription,
}
// set-like map to quickly check if a parameter is supported

View File

@@ -438,7 +438,7 @@ func (o RegistryClient) retrieveDevfileDataFromRegistry(ctx context.Context, reg
devfileYamlFile := location.DevfileFilenamesProvider(tmpFile)
// Parse and validate the file and return the devfile data
devfileObj, err := devfile.ParseAndValidateFromFile(path.Join(tmpFile, devfileYamlFile))
devfileObj, err := devfile.ParseAndValidateFromFile(path.Join(tmpFile, devfileYamlFile), "", true)
if err != nil {
return api.DevfileData{}, err
}

View File

@@ -8,12 +8,12 @@ import (
v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
devfilepkg "github.com/devfile/api/v2/pkg/devfile"
"github.com/devfile/library/v2/pkg/devfile"
"github.com/devfile/library/v2/pkg/devfile/parser"
devfileCtx "github.com/devfile/library/v2/pkg/devfile/parser/context"
"github.com/devfile/library/v2/pkg/devfile/parser/data"
devfilefs "github.com/devfile/library/v2/pkg/testingutil/filesystem"
"k8s.io/utils/pointer"
"github.com/redhat-developer/odo/pkg/devfile"
)
// GetFakeContainerComponent returns a fake container component for testing
@@ -143,10 +143,7 @@ func GetTestDevfileObjFromFile(fileName string) parser.DevfileObj {
// path to the devfile
devfilePath := filepath.Join(filepath.Dir(filename), "..", "..", "tests", "examples", filepath.Join("source", "devfiles", "nodejs", fileName))
devfileObj, _, err := devfile.ParseDevfileAndValidate(parser.ParserArgs{
Path: devfilePath,
SetBooleanDefaults: pointer.Bool(false),
})
devfileObj, err := devfile.ParseAndValidateFromFile(devfilePath, "", false)
if err != nil {
return parser.DevfileObj{}
}

View File

@@ -91,7 +91,7 @@ type processEventsFunc func(ctx context.Context, parameters WatchParameters, cha
func (o *WatchClient) WatchAndPush(ctx context.Context, parameters WatchParameters, componentStatus ComponentStatus) error {
var (
devfileObj = odocontext.GetDevfileObj(ctx)
devfileObj = odocontext.GetEffectiveDevfileObj(ctx)
devfilePath = odocontext.GetDevfilePath(ctx)
path = filepath.Dir(devfilePath)
componentName = odocontext.GetComponentName(ctx)

View File

@@ -0,0 +1,158 @@
schemaVersion: 2.2.0
metadata:
name: my-image-name-as-selector
version: 1.0.0
starterProjects:
- git:
remotes:
origin: https://github.com/odo-devfiles/nodejs-ex.git
name: nodejs-starter
variables:
# Relative image name, which should be handled as a selector
CONTAINER_IMAGE_RELATIVE: "nodejs-devtools"
# Relative image name not matching relative image above and not built by an image component => should not be replaced
CONTAINER_IMAGE_RELATIVE_NOT_MATCHING_AND_NOT_USED_IN_IMAGE_COMP: "nodejs-devtools007"
# Absolute image name, which should not be handled as a selector
CONTAINER_IMAGE_ABSOLUTE: "ttl.sh/odo-dev-node:1h"
# Absolute image name not matching relative image above => should not be replaced
CONTAINER_IMAGE_ABSOLUTE_NOT_MATCHING_RELATIVE: "ttl.sh/nodejs-devtools2:1h"
commands:
- exec:
commandLine: npm install
component: runtime
group:
isDefault: true
kind: build
workingDir: ${PROJECT_SOURCE}
id: build
- exec:
commandLine: npm run start
component: runtime
workingDir: ${PROJECT_SOURCE}
group:
isDefault: true
kind: run
id: start
- exec:
commandLine: npm run debug
component: runtime
workingDir: ${PROJECT_SOURCE}
group:
isDefault: true
kind: debug
id: debug
- composite:
# Just to automatically apply components with autoBuild=true, deployByDefault=true or not referenced
commands: []
group:
isDefault: true
kind: deploy
id: deploy
components:
- image:
autoBuild: true
dockerfile:
buildContext: .
uri: Dockerfile
imageName: "{{ CONTAINER_IMAGE_RELATIVE }}:1.2.3-my-tag"
name: relative-image
- image:
autoBuild: true
dockerfile:
buildContext: .
uri: Dockerfile
imageName: "{{ CONTAINER_IMAGE_ABSOLUTE }}"
name: absolute-image
- image:
autoBuild: true
dockerfile:
buildContext: .
uri: Dockerfile
imageName: "{{ CONTAINER_IMAGE_ABSOLUTE_NOT_MATCHING_RELATIVE }}"
name: absolute-image-not-matching-relative
- container:
command: [ 'tail' ]
args: [ '-f', '/dev/null' ]
endpoints:
- name: "8080-tcp"
targetPort: 8080
- name: "debug"
targetPort: 5858
exposure: none
env:
- name: DEBUG_PORT_PROJECT
value: "5858"
image: registry.access.redhat.com/ubi8/nodejs-16:latest
# # Relative image matches the imageName field of the 'relative-image' Image Component => it will be replaced dynamically regardless of the registry, tag or digest
# image: "quay.io/{{ CONTAINER_IMAGE_RELATIVE }}@sha256:26c68657ccce2cb0a31b330cb0be2b5e108d467f641c62e13ab40cbec258c68d"
memoryLimit: 1024Mi
mountSources: true
name: runtime
- name: k8s-uri
kubernetes:
deployByDefault: true
uri: kubernetes/devfile-image-names-as-selectors/k8s.yaml
- name: ocp-inlined
openshift:
deployByDefault: true
inlined: |
apiVersion: batch/v1
kind: Job
metadata:
name: my-ocp-job
spec:
template:
metadata:
name: my-app
spec:
restartPolicy: Never
containers:
# Relative image matches the imageName field of the 'relative-image' Image Component
# => it will be replaced dynamically regardless of the registry, tag or digest
- image: "quay.io/{{ CONTAINER_IMAGE_RELATIVE }}@sha256:26c68657ccce2cb0a31b330cb0be2b5e108d467f641c62e13ab40cbec258c68d"
name: my-main-cont1
- image: "{{ CONTAINER_IMAGE_ABSOLUTE_NOT_MATCHING_RELATIVE }}"
name: my-main-cont2
initContainers:
- image: "{{ CONTAINER_IMAGE_ABSOLUTE }}"
name: my-init-cont1
- image: "{{ CONTAINER_IMAGE_RELATIVE_NOT_MATCHING_AND_NOT_USED_IN_IMAGE_COMP }}"
name: my-init-cont2
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: my-ocp-cron-job
spec:
concurrencyPolicy: Forbid
schedule: '*/1 * * * *'
jobTemplate:
spec:
template:
metadata:
name: my-app
spec:
restartPolicy: Never
containers:
- image: "{{ CONTAINER_IMAGE_RELATIVE }}"
name: my-main-cont1
- image: "{{ CONTAINER_IMAGE_ABSOLUTE_NOT_MATCHING_RELATIVE }}"
name: my-main-cont2
initContainers:
- image: "{{ CONTAINER_IMAGE_ABSOLUTE }}"
name: my-init-cont1
- image: "{{ CONTAINER_IMAGE_RELATIVE_NOT_MATCHING_AND_NOT_USED_IN_IMAGE_COMP }}"
name: my-init-cont2

View File

@@ -0,0 +1,156 @@
apiVersion: v1
kind: Pod
metadata:
name: my-k8s-pod
spec:
containers:
- image: "{{ CONTAINER_IMAGE_RELATIVE }}"
name: my-main-cont1
- image: "{{ CONTAINER_IMAGE_ABSOLUTE_NOT_MATCHING_RELATIVE }}"
name: my-main-cont2
initContainers:
- image: "{{ CONTAINER_IMAGE_ABSOLUTE }}"
name: my-init-cont1
- image: "{{ CONTAINER_IMAGE_RELATIVE_NOT_MATCHING_AND_NOT_USED_IN_IMAGE_COMP }}"
name: my-init-cont2
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: my-app
name: my-k8s-deployment
spec:
replicas: 1
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- image: "{{ CONTAINER_IMAGE_RELATIVE }}"
name: my-main-cont1
- image: "{{ CONTAINER_IMAGE_ABSOLUTE_NOT_MATCHING_RELATIVE }}"
name: my-main-cont2
initContainers:
- image: "{{ CONTAINER_IMAGE_ABSOLUTE }}"
name: my-init-cont1
- image: "{{ CONTAINER_IMAGE_RELATIVE_NOT_MATCHING_AND_NOT_USED_IN_IMAGE_COMP }}"
name: my-init-cont2
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
labels:
app: my-app
name: my-k8s-daemonset
spec:
selector:
matchLabels:
name: my-app
template:
metadata:
labels:
creationTimestamp: ""
name: my-app
spec:
containers:
- image: "{{ CONTAINER_IMAGE_RELATIVE }}"
name: my-main-cont1
- image: "{{ CONTAINER_IMAGE_ABSOLUTE_NOT_MATCHING_RELATIVE }}"
name: my-main-cont2
initContainers:
- image: "{{ CONTAINER_IMAGE_ABSOLUTE }}"
name: my-init-cont1
- image: "{{ CONTAINER_IMAGE_RELATIVE_NOT_MATCHING_AND_NOT_USED_IN_IMAGE_COMP }}"
name: my-init-cont2
---
apiVersion: apps/v1
kind: ReplicaSet
metadata:
labels:
app: my-app
name: my-k8s-replicaset
spec:
replicas: 1
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- image: "{{ CONTAINER_IMAGE_RELATIVE }}"
name: my-main-cont1
- image: "{{ CONTAINER_IMAGE_ABSOLUTE_NOT_MATCHING_RELATIVE }}"
name: my-main-cont2
initContainers:
- image: "{{ CONTAINER_IMAGE_ABSOLUTE }}"
name: my-init-cont1
- image: "{{ CONTAINER_IMAGE_RELATIVE_NOT_MATCHING_AND_NOT_USED_IN_IMAGE_COMP }}"
name: my-init-cont2
---
apiVersion: v1
kind: ReplicationController
metadata:
name: my-k8s-replicationcontroller
spec:
replicas: 3
selector:
app: my-app
template:
metadata:
labels:
app: my-app
name: my-app
spec:
containers:
- image: "{{ CONTAINER_IMAGE_RELATIVE }}"
name: my-main-cont1
- image: "{{ CONTAINER_IMAGE_ABSOLUTE_NOT_MATCHING_RELATIVE }}"
name: my-main-cont2
initContainers:
- image: "{{ CONTAINER_IMAGE_ABSOLUTE }}"
name: my-init-cont1
- image: "{{ CONTAINER_IMAGE_RELATIVE_NOT_MATCHING_AND_NOT_USED_IN_IMAGE_COMP }}"
name: my-init-cont2
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-k8s-statefulset
spec:
replicas: 1
selector:
matchLabels:
app: my-app
serviceName: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- image: "{{ CONTAINER_IMAGE_RELATIVE }}"
name: my-main-cont1
- image: "{{ CONTAINER_IMAGE_ABSOLUTE_NOT_MATCHING_RELATIVE }}"
name: my-main-cont2
initContainers:
- image: "{{ CONTAINER_IMAGE_ABSOLUTE }}"
name: my-init-cont1
- image: "{{ CONTAINER_IMAGE_RELATIVE_NOT_MATCHING_AND_NOT_USED_IN_IMAGE_COMP }}"
name: my-init-cont2
# TODO(rm3l): test with some Custom Resources as well. Bug odo if comment is laced after last ---
---

View File

@@ -51,13 +51,13 @@ func DeleteProject(projectName string) {
// GetMetadataFromDevfile retrieves the metadata from devfile
func GetMetadataFromDevfile(devfilePath string) devfilepkg.DevfileMetadata {
devObj, err := devfile.ParseAndValidateFromFile(devfilePath)
devObj, err := devfile.ParseAndValidateFromFile(devfilePath, "", true)
Expect(err).ToNot(HaveOccurred())
return devObj.Data.GetMetadata()
}
func GetDevfileComponents(devfilePath, componentName string) []v1alpha2.Component {
devObj, err := devfile.ParseAndValidateFromFile(devfilePath)
devObj, err := devfile.ParseAndValidateFromFile(devfilePath, "", true)
Expect(err).ToNot(HaveOccurred())
components, err := devObj.Data.GetComponents(common.DevfileOptions{
FilterByName: componentName,

View File

@@ -2,6 +2,7 @@ package integration
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
@@ -4544,4 +4545,227 @@ CMD ["npm", "start"]
}))
}
for _, podman := range []bool{false, true} {
podman := podman
Context("image names as selectors", helper.LabelPodmanIf(podman, func() {
When("starting with a Devfile with relative and absolute image names and Kubernetes resources", func() {
BeforeEach(func() {
helper.CopyExample(filepath.Join("source", "nodejs"), commonVar.Context)
helper.CopyExample(
filepath.Join("source", "devfiles", "nodejs", "kubernetes", "devfile-image-names-as-selectors"),
filepath.Join(commonVar.Context, "kubernetes", "devfile-image-names-as-selectors"))
helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "nodejs", "devfile-image-names-as-selectors.yaml"),
filepath.Join(commonVar.Context, "devfile.yaml"),
helper.DevfileMetadataNameSetter(cmpName))
})
When("adding a local registry for images", func() {
const imageRegistry = "ttl.sh"
BeforeEach(func() {
helper.Cmd("odo", "preference", "set", "ImageRegistry", imageRegistry, "--force").ShouldPass()
})
AfterEach(func() {
helper.Cmd("odo", "preference", "unset", "ImageRegistry", "--force").ShouldPass()
})
extractContainerNameImageMapFn := func(resourceType, resourceName, jsonPath string) map[string]string {
result := make(map[string]string)
data := commonVar.CliRunner.Run("-n", commonVar.Project, "get", resourceType, resourceName,
"-o", fmt.Sprintf("jsonpath=%s", jsonPath)).Out.Contents()
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
l := scanner.Text()
name, image, found := strings.Cut(l, " ")
if !found {
continue
}
result[name] = image
}
return result
}
When("running odo dev", func() {
var devSession helper.DevSession
var stdout string
BeforeEach(func() {
var env []string
if podman {
env = append(env, "ODO_PUSH_IMAGES=false")
} else {
env = append(env, "PODMAN_CMD=echo")
}
var outB []byte
var err error
devSession, outB, _, _, err = helper.StartDevMode(helper.DevSessionOpts{
RunOnPodman: podman,
EnvVars: env,
})
Expect(err).ShouldNot(HaveOccurred())
stdout = string(outB)
})
AfterEach(func() {
devSession.Stop()
if podman {
devSession.WaitEnd()
}
})
It("should treat relative image names as selectors", func() {
imageMessagePrefix := "Building & Pushing Image"
if podman {
imageMessagePrefix = "Building Image"
}
lines, err := helper.ExtractLines(stdout)
Expect(err).ShouldNot(HaveOccurred())
var replacementImageName string
var imagesProcessed []string
re := regexp.MustCompile(fmt.Sprintf(`(?:%s):\s*([^\n]+)`, imageMessagePrefix))
replaceImageRe := regexp.MustCompile(fmt.Sprintf("%s/%s-nodejs-devtools:[^\n]+", imageRegistry, cmpName))
for _, l := range lines {
matches := re.FindStringSubmatch(l)
if len(matches) > 1 {
img := matches[1]
imagesProcessed = append(imagesProcessed, img)
if replaceImageRe.MatchString(img) {
replacementImageName = img
}
}
}
By("building and optionally pushing relative image components", func() {
Expect(replacementImageName).ShouldNot(BeEmpty(), "could not find image matching regexp %v", replaceImageRe)
Expect(imagesProcessed).Should(ContainElement(
MatchRegexp(fmt.Sprintf("%s/%s-nodejs-devtools:[^\n]+", imageRegistry, cmpName))))
})
By("building and optionally pushing absolute image components with no replacement", func() {
for _, img := range []string{"ttl.sh/odo-dev-node:1h", "ttl.sh/nodejs-devtools2:1h"} {
Expect(imagesProcessed).Should(ContainElement(img))
}
})
if !podman {
// On Podman, `odo dev` just warns if there are any Kubernetes/OpenShift components at the moment. But this is already tested elsewhere
// and not useful to test here.
// But we should definitely test it if we plan on supporting more K8s resources from those components.
type resourceData struct {
containers map[string]string
initContainers map[string]string
}
k8sResourcesDeployed := map[string]resourceData{
"CronJob": {
containers: extractContainerNameImageMapFn("CronJob", "my-ocp-cron-job",
"{range .spec.jobTemplate.spec.template.spec.containers[*]}{.name} {.image}{\"\\n\"}{end}"),
initContainers: extractContainerNameImageMapFn("CronJob", "my-ocp-cron-job",
"{range .spec.jobTemplate.spec.template.spec.initContainers[*]}{.name} {.image}{\"\\n\"}{end}"),
},
"DaemonSet": {
containers: extractContainerNameImageMapFn("DaemonSet", "my-k8s-daemonset",
"{range .spec.template.spec.containers[*]}{.name} {.image}{\"\\n\"}{end}"),
initContainers: extractContainerNameImageMapFn("DaemonSet", "my-k8s-daemonset",
"{range .spec.template.spec.initContainers[*]}{.name} {.image}{\"\\n\"}{end}"),
},
"Deployment": {
containers: extractContainerNameImageMapFn("Deployment", "my-k8s-deployment",
"{range .spec.template.spec.containers[*]}{.name} {.image}{\"\\n\"}{end}"),
initContainers: extractContainerNameImageMapFn("Deployment", "my-k8s-deployment",
"{range .spec.template.spec.initContainers[*]}{.name} {.image}{\"\\n\"}{end}"),
},
"Job": {
containers: extractContainerNameImageMapFn("Job", "my-ocp-job",
"{range .spec.template.spec.containers[*]}{.name} {.image}{\"\\n\"}{end}"),
initContainers: extractContainerNameImageMapFn("Job", "my-ocp-job",
"{range .spec.template.spec.initContainers[*]}{.name} {.image}{\"\\n\"}{end}"),
},
"Pod": {
containers: extractContainerNameImageMapFn("Pod", "my-k8s-pod",
"{range .spec.containers[*]}{.name} {.image}{\"\\n\"}{end}"),
initContainers: extractContainerNameImageMapFn("Pod", "my-k8s-pod",
"{range .spec.initContainers[*]}{.name} {.image}{\"\\n\"}{end}"),
},
"ReplicaSet": {
containers: extractContainerNameImageMapFn("ReplicaSet", "my-k8s-replicaset",
"{range .spec.template.spec.containers[*]}{.name} {.image}{\"\\n\"}{end}"),
initContainers: extractContainerNameImageMapFn("ReplicaSet", "my-k8s-replicaset",
"{range .spec.template.spec.initContainers[*]}{.name} {.image}{\"\\n\"}{end}"),
},
"ReplicationController": {
containers: extractContainerNameImageMapFn("ReplicationController", "my-k8s-replicationcontroller",
"{range .spec.template.spec.containers[*]}{.name} {.image}{\"\\n\"}{end}"),
initContainers: extractContainerNameImageMapFn("ReplicationController", "my-k8s-replicationcontroller",
"{range .spec.template.spec.initContainers[*]}{.name} {.image}{\"\\n\"}{end}"),
},
"StatefulSet": {
containers: extractContainerNameImageMapFn("StatefulSet", "my-k8s-statefulset",
"{range .spec.template.spec.containers[*]}{.name} {.image}{\"\\n\"}{end}"),
initContainers: extractContainerNameImageMapFn("StatefulSet", "my-k8s-statefulset",
"{range .spec.template.spec.initContainers[*]}{.name} {.image}{\"\\n\"}{end}"),
},
}
By("replacing matching image names in core Kubernetes components", func() {
const mainCont1 = "my-main-cont1"
for resType, data := range k8sResourcesDeployed {
Expect(data.containers[mainCont1]).Should(
Equal(replacementImageName),
func() string {
return fmt.Sprintf(
"unexpected image for container %q in %q deployed from K8s or OCP component. All resources: %v",
mainCont1, resType, k8sResourcesDeployed)
})
}
})
By("not replacing non-matching or absolute image names in core Kubernetes resources", func() {
const (
mainCont2 = "my-main-cont2"
initCont1 = "my-init-cont1"
initCont2 = "my-init-cont2"
)
for resType, data := range k8sResourcesDeployed {
Expect(data.containers[mainCont2]).Should(
Equal("ttl.sh/nodejs-devtools2:1h"),
func() string {
return fmt.Sprintf(
"unexpected image for container %q in %q deployed from K8s or OCP component. All resources: %v",
mainCont2, resType, k8sResourcesDeployed)
})
Expect(data.initContainers[initCont1]).Should(
Equal("ttl.sh/odo-dev-node:1h"),
func() string {
return fmt.Sprintf(
"unexpected image for init container %q in %q deployed from K8s or OCP component. All resources: %v",
initCont1, resType, k8sResourcesDeployed)
})
Expect(data.initContainers[initCont2]).Should(
Equal("nodejs-devtools007"),
func() string {
return fmt.Sprintf(
"unexpected image for init container %q in %q deployed from K8s or OCP component. All resources: %v",
initCont1, resType, k8sResourcesDeployed)
})
}
})
}
})
})
})
})
}))
}
})

View File

@@ -144,8 +144,11 @@ type ExecCommand struct {
Env []EnvVar `json:"env,omitempty" patchStrategy:"merge" patchMergeKey:"name"`
// +optional
// Whether the command is capable to reload itself when source code changes.
// If set to `true` the command won't be restarted and it is expected to handle file changes on its own.
// Specify whether the command is restarted or not when the source code changes.
// If set to `true` the command won't be restarted.
// A *hotReloadCapable* `run` or `debug` command is expected to handle file changes on its own and won't be restarted.
// A *hotReloadCapable* `build` command is expected to be executed only once and won't be executed again.
// This field is taken into account only for commands `build`, `run` and `debug` with `isDefault` set to `true`.
//
// Default value is `false`
// +devfile:default:value=false

View File

@@ -94,10 +94,7 @@ func (in *Container) GetMountSources() bool {
if in.MountSources != nil {
return *in.MountSources
} else {
if in.GetDedicatedPod() {
return false
}
return true
return !in.GetDedicatedPod()
}
}

View File

@@ -2,5 +2,5 @@
// +k8s:deepcopy-gen=package,register
// +k8s:openapi-gen=true
// +groupName=workspace.devfile.io
// +devfile:jsonschema:version=2.2.0
// +devfile:jsonschema:version=2.2.1-alpha
package v1alpha2

View File

@@ -34,7 +34,7 @@ func visitUnion(union interface{}, visitor interface{}) (err error) {
}
func simplifyUnion(union Union, visitorType reflect.Type) {
normalizeUnion(union, visitorType)
_ = normalizeUnion(union, visitorType)
*union.discriminator() = ""
}

View File

@@ -56,7 +56,7 @@ type ParentOverrides struct {
Commands []CommandParentOverride `json:"commands,omitempty" patchStrategy:"merge" patchMergeKey:"id"`
}
//+k8s:openapi-gen=true
// +k8s:openapi-gen=true
type ComponentParentOverride struct {
// Mandatory name that allows referencing the component
@@ -336,8 +336,11 @@ type ExecCommandParentOverride struct {
Env []EnvVarParentOverride `json:"env,omitempty" patchStrategy:"merge" patchMergeKey:"name"`
// +optional
// Whether the command is capable to reload itself when source code changes.
// If set to `true` the command won't be restarted and it is expected to handle file changes on its own.
// Specify whether the command is restarted or not when the source code changes.
// If set to `true` the command won't be restarted.
// A *hotReloadCapable* `run` or `debug` command is expected to handle file changes on its own and won't be restarted.
// A *hotReloadCapable* `build` command is expected to be executed only once and won't be executed again.
// This field is taken into account only for commands `build`, `run` and `debug` with `isDefault` set to `true`.
//
// Default value is `false`
HotReloadCapable *bool `json:"hotReloadCapable,omitempty"`
@@ -721,7 +724,7 @@ type ImportReferenceUnionParentOverride struct {
// So please be careful when renaming
type OverridesBaseParentOverride struct{}
//+k8s:openapi-gen=true
// +k8s:openapi-gen=true
type ComponentPluginOverrideParentOverride struct {
// Mandatory name that allows referencing the component
@@ -1005,8 +1008,11 @@ type ExecCommandPluginOverrideParentOverride struct {
Env []EnvVarPluginOverrideParentOverride `json:"env,omitempty" patchStrategy:"merge" patchMergeKey:"name"`
// +optional
// Whether the command is capable to reload itself when source code changes.
// If set to `true` the command won't be restarted and it is expected to handle file changes on its own.
// Specify whether the command is restarted or not when the source code changes.
// If set to `true` the command won't be restarted.
// A *hotReloadCapable* `run` or `debug` command is expected to handle file changes on its own and won't be restarted.
// A *hotReloadCapable* `build` command is expected to be executed only once and won't be executed again.
// This field is taken into account only for commands `build`, `run` and `debug` with `isDefault` set to `true`.
//
// Default value is `false`
HotReloadCapable *bool `json:"hotReloadCapable,omitempty"`

View File

@@ -25,7 +25,7 @@ type PluginOverrides struct {
Commands []CommandPluginOverride `json:"commands,omitempty" patchStrategy:"merge" patchMergeKey:"id"`
}
//+k8s:openapi-gen=true
// +k8s:openapi-gen=true
type ComponentPluginOverride struct {
// Mandatory name that allows referencing the component
@@ -206,8 +206,11 @@ type ExecCommandPluginOverride struct {
Env []EnvVarPluginOverride `json:"env,omitempty" patchStrategy:"merge" patchMergeKey:"name"`
// +optional
// Whether the command is capable to reload itself when source code changes.
// If set to `true` the command won't be restarted and it is expected to handle file changes on its own.
// Specify whether the command is restarted or not when the source code changes.
// If set to `true` the command won't be restarted.
// A *hotReloadCapable* `run` or `debug` command is expected to handle file changes on its own and won't be restarted.
// A *hotReloadCapable* `build` command is expected to be executed only once and won't be executed again.
// This field is taken into account only for commands `build`, `run` and `debug` with `isDefault` set to `true`.
//
// Default value is `false`
HotReloadCapable *bool `json:"hotReloadCapable,omitempty"`

View File

@@ -16,7 +16,7 @@ func (n *normalizer) Struct(s reflect.Value) error {
if addr.CanInterface() {
i := addr.Interface()
if u, ok := i.(dw.Union); ok {
u.Normalize()
_ = u.Normalize()
}
}
}

View File

@@ -149,8 +149,8 @@ func ValidateComponents(components []v1alpha2.Component) (returnedErr error) {
returnedErr = multierror.Append(returnedErr, resolveErrorMessageWithImportAttributes(err, component.Attributes))
}
}
err := validateEndpoints(component.Openshift.Endpoints, processedEndPointPort, processedEndPointName)
currentComponentEndPointPort := make(map[int]bool)
err := validateDuplicatedName(component.Openshift.Endpoints, processedEndPointName, currentComponentEndPointPort)
if len(err) > 0 {
for _, endpointErr := range err {
returnedErr = multierror.Append(returnedErr, resolveErrorMessageWithImportAttributes(endpointErr, component.Attributes))
@@ -163,7 +163,8 @@ func ValidateComponents(components []v1alpha2.Component) (returnedErr error) {
returnedErr = multierror.Append(returnedErr, resolveErrorMessageWithImportAttributes(err, component.Attributes))
}
}
err := validateEndpoints(component.Kubernetes.Endpoints, processedEndPointPort, processedEndPointName)
currentComponentEndPointPort := make(map[int]bool)
err := validateDuplicatedName(component.Kubernetes.Endpoints, processedEndPointName, currentComponentEndPointPort)
if len(err) > 0 {
for _, endpointErr := range err {
returnedErr = multierror.Append(returnedErr, resolveErrorMessageWithImportAttributes(endpointErr, component.Attributes))

View File

@@ -10,6 +10,14 @@ import "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
func validateEndpoints(endpoints []v1alpha2.Endpoint, processedEndPointPort map[int]bool, processedEndPointName map[string]bool) (errList []error) {
currentComponentEndPointPort := make(map[int]bool)
errList = validateDuplicatedName(endpoints, processedEndPointName, currentComponentEndPointPort)
portErrorList := validateDuplicatedPort(processedEndPointPort, currentComponentEndPointPort)
errList = append(errList, portErrorList...)
return errList
}
func validateDuplicatedName(endpoints []v1alpha2.Endpoint, processedEndPointName map[string]bool, currentComponentEndPointPort map[int]bool) (errList []error) {
for _, endPoint := range endpoints {
if _, ok := processedEndPointName[endPoint.Name]; ok {
errList = append(errList, &InvalidEndpointError{name: endPoint.Name})
@@ -17,13 +25,15 @@ func validateEndpoints(endpoints []v1alpha2.Endpoint, processedEndPointPort map[
processedEndPointName[endPoint.Name] = true
currentComponentEndPointPort[endPoint.TargetPort] = true
}
return errList
}
func validateDuplicatedPort(processedEndPointPort map[int]bool, currentComponentEndPointPort map[int]bool) (errList []error) {
for targetPort := range currentComponentEndPointPort {
if _, ok := processedEndPointPort[targetPort]; ok {
errList = append(errList, &InvalidEndpointError{port: targetPort})
}
processedEndPointPort[targetPort] = true
}
return errList
}

View File

@@ -14,7 +14,7 @@ The validation will be done as part of schema validation, the rule will be intro
### Endpoints:
- all the endpoint names are unique across components
- endpoint ports must be unique across components -- two components cannot have the same target port, but one component may have two endpoints with the same target port. This restriction does not apply to container components with `dedicatedPod` set to `true`.
- endpoint ports must be unique across container components -- two container components cannot have the same target port, but one container component may have two endpoints with the same target port. This restriction does not apply to container components with `dedicatedPod` set to `true`.
### Commands:

View File

@@ -0,0 +1,304 @@
//
// Copyright 2023 Red Hat, 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 devfile
import (
"bufio"
"bytes"
"fmt"
"io"
"strings"
v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/library/v2/pkg/devfile/parser"
"github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common"
"github.com/distribution/distribution/v3/reference"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
utilyaml "k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/kubernetes/scheme"
)
var k8sSerializer = json.NewSerializerWithOptions(
json.DefaultMetaFactory,
scheme.Scheme,
scheme.Scheme,
json.SerializerOptions{
Yaml: true,
Pretty: true,
})
// replaceImageNames parses all Image components in the specified Devfile object and,
// for each relative image name, replaces the value in all matching Image, Container and Kubernetes/Openshift components.
//
// An image is said to be relative if it has a canonical name different from its actual name.
// For example, image names like 'nodejs-devtools', 'nodejs-devtools:some-tag', 'nodejs-devtools@digest', or even 'some_name_different_from_localhost/nodejs-devtools' are all relative because
// their canonical form (as returned by the Distribution library) will be prefixed with 'docker.io/library/'.
// On the other hand, image names like 'docker.io/library/nodejs-devtools', 'localhost/nodejs-devtools@digest' or 'quay.io/nodejs-devtools:some-tag' are absolute.
//
// A component is said to be matching if the base name of the image used in this component is the same as the base name of the image component, regardless of its tag, digest or registry.
// For example, if the Devfile has an Image component with an image named 'nodejs-devtools' and 2 Container components using an image named 'nodejs-devtools:some-tag' and another absolute image named
// 'quay.io/nodejs-devtools@digest', both image names in the two Container components will be replaced by a value described below (because the base names of those images are 'nodejs-devtools', which
// match the base name of the relative image name of the Image Component).
// But `nodejs-devtools2` or 'ghcr.io/some-user/nodejs-devtools3' do not match the 'nodejs-devtools' image name and won't be replaced.
//
// For Kubernetes and OpenShift components, this function assumes that the actual resource manifests are inlined in the components,
// in order to perform any replacements for matching image names.
// At the moment, this function only supports replacements in Kubernetes native resource types (Pod, CronJob, Job, DaemonSet; Deployment, ReplicaSet, ReplicationController, StatefulSet).
//
// Absolute images and non-matching image references are left unchanged.
//
// And the replacement is done by using the following format: "<registry>/<devfileName>-<baseImageName>:<imageTag>",
// where both <registry> and <imageTag> are set by the tool itself (either via auto-detection or via user input).
func replaceImageNames(d *parser.DevfileObj, registry string, imageTag string) (err error) {
var imageComponents []v1.Component
imageComponents, err = d.Data.GetComponents(common.DevfileOptions{
ComponentOptions: common.ComponentOptions{ComponentType: v1.ImageComponentType},
})
if err != nil {
return err
}
var isAbs bool
var imageRef reference.Named
for _, comp := range imageComponents {
imageName := comp.Image.ImageName
isAbs, imageRef, err = parseImageReference(imageName)
if err != nil {
return err
}
if isAbs {
continue
}
baseImageName := getImageSimpleName(imageRef)
replacement := baseImageName
if d.GetMetadataName() != "" {
replacement = fmt.Sprintf("%s-%s", d.GetMetadataName(), replacement)
}
if registry != "" {
replacement = fmt.Sprintf("%s/%s", strings.TrimSuffix(registry, "/"), replacement)
}
if imageTag != "" {
replacement += fmt.Sprintf(":%s", imageTag)
}
// Replace so that the image can be built and pushed to the registry specified by the tool.
comp.Image.ImageName = replacement
// Replace in matching container components
err = handleContainerComponents(d, baseImageName, replacement)
if err != nil {
return err
}
// Replace in matching Kubernetes and OpenShift components
err = handleKubernetesLikeComponents(d, baseImageName, replacement)
if err != nil {
return err
}
}
return nil
}
// parseImageReference uses the Docker reference library to detect if the image name is absolute or not
// and returns a struct from which we can extract the domain, tag and digest if needed.
func parseImageReference(imageName string) (isAbsolute bool, imageRef reference.Named, err error) {
imageRef, err = reference.ParseNormalizedNamed(imageName)
if err != nil {
return false, nil, err
}
// Non-canonical image references are not absolute.
// For example, "nodejs-devtools" will be parsed as "docker.io/library/nodejs-devtools"
isAbsolute = imageRef.String() == imageName
return isAbsolute, imageRef, nil
}
func getImageSimpleName(img reference.Named) string {
p := reference.Path(img)
i := strings.LastIndex(p, "/")
result := p
if i >= 0 {
result = strings.TrimPrefix(p[i:], "/")
}
return result
}
func hasMatch(baseImageName, compImage string) (bool, error) {
_, imageRef, err := parseImageReference(compImage)
if err != nil {
return false, err
}
return getImageSimpleName(imageRef) == baseImageName, nil
}
func handleContainerComponents(d *parser.DevfileObj, baseImageName, replacement string) (err error) {
var containerComponents []v1.Component
containerComponents, err = d.Data.GetComponents(common.DevfileOptions{
ComponentOptions: common.ComponentOptions{ComponentType: v1.ContainerComponentType},
})
if err != nil {
return err
}
for _, comp := range containerComponents {
var match bool
match, err = hasMatch(baseImageName, comp.Container.Image)
if err != nil {
return err
}
if !match {
continue
}
comp.Container.Image = replacement
}
return nil
}
func handleKubernetesLikeComponents(d *parser.DevfileObj, baseImageName, replacement string) error {
var allK8sOcComponents []v1.Component
k8sComponents, err := d.Data.GetComponents(common.DevfileOptions{
ComponentOptions: common.ComponentOptions{ComponentType: v1.KubernetesComponentType},
})
if err != nil {
return err
}
allK8sOcComponents = append(allK8sOcComponents, k8sComponents...)
ocComponents, err := d.Data.GetComponents(common.DevfileOptions{
ComponentOptions: common.ComponentOptions{ComponentType: v1.OpenshiftComponentType},
})
if err != nil {
return err
}
allK8sOcComponents = append(allK8sOcComponents, ocComponents...)
updateImageInPodSpecIfNeeded := func(obj runtime.Object, ps *corev1.PodSpec) (string, error) {
handleContainer := func(c *corev1.Container) (match bool, err error) {
match, err = hasMatch(baseImageName, c.Image)
if err != nil {
return false, err
}
if !match {
return false, nil
}
c.Image = replacement
return true, nil
}
for i := range ps.Containers {
if _, err = handleContainer(&ps.Containers[i]); err != nil {
return "", err
}
}
for i := range ps.InitContainers {
if _, err = handleContainer(&ps.InitContainers[i]); err != nil {
return "", err
}
}
for i := range ps.EphemeralContainers {
if _, err = handleContainer((*corev1.Container)(&ps.EphemeralContainers[i].EphemeralContainerCommon)); err != nil {
return "", err
}
}
//Encode obj back into a YAML string
var s strings.Builder
err = k8sSerializer.Encode(obj, &s)
if err != nil {
return "", err
}
return s.String(), nil
}
handleK8sContent := func(content string) (newContent string, err error) {
multidocReader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewBufferString(content)))
var yamlAsStringList []string
var buf []byte
var obj runtime.Object
for {
buf, err = multidocReader.Read()
if err != nil {
if err == io.EOF {
break
}
return "", err
}
obj, _, err = k8sSerializer.Decode(buf, nil, nil)
if err != nil {
// Use raw string as it is, as it might be a Custom Resource with a Kind that is not known
// by the K8s decoder.
yamlAsStringList = append(yamlAsStringList, strings.TrimSpace(string(buf)))
continue
}
newYaml := string(buf)
switch r := obj.(type) {
case *batchv1.CronJob:
newYaml, err = updateImageInPodSpecIfNeeded(r, &r.Spec.JobTemplate.Spec.Template.Spec)
case *appsv1.DaemonSet:
newYaml, err = updateImageInPodSpecIfNeeded(r, &r.Spec.Template.Spec)
case *appsv1.Deployment:
newYaml, err = updateImageInPodSpecIfNeeded(r, &r.Spec.Template.Spec)
case *batchv1.Job:
newYaml, err = updateImageInPodSpecIfNeeded(r, &r.Spec.Template.Spec)
case *corev1.Pod:
newYaml, err = updateImageInPodSpecIfNeeded(r, &r.Spec)
case *appsv1.ReplicaSet:
newYaml, err = updateImageInPodSpecIfNeeded(r, &r.Spec.Template.Spec)
case *corev1.ReplicationController:
newYaml, err = updateImageInPodSpecIfNeeded(r, &r.Spec.Template.Spec)
case *appsv1.StatefulSet:
newYaml, err = updateImageInPodSpecIfNeeded(r, &r.Spec.Template.Spec)
}
if err != nil {
return "", err
}
yamlAsStringList = append(yamlAsStringList, strings.TrimSpace(newYaml))
}
return strings.Join(yamlAsStringList, "\n---\n"), nil
}
var newContent string
for _, comp := range allK8sOcComponents {
if comp.Kubernetes != nil {
newContent, err = handleK8sContent(comp.Kubernetes.Inlined)
if err != nil {
return err
}
comp.Kubernetes.Inlined = newContent
} else {
newContent, err = handleK8sContent(comp.Openshift.Inlined)
if err != nil {
return err
}
comp.Openshift.Inlined = newContent
}
}
return nil
}

View File

@@ -109,6 +109,15 @@ func ParseDevfileAndValidate(args parser.ParserArgs) (d parser.DevfileObj, varWa
varWarning = variables.ValidateAndReplaceGlobalVariable(d.Data.GetDevfileWorkspaceSpec())
}
// Use image names as selectors after variable substitution,
// as users might be using variables for image names.
if args.ImageNamesAsSelector != nil && args.ImageNamesAsSelector.Registry != "" {
err = replaceImageNames(&d, args.ImageNamesAsSelector.Registry, args.ImageNamesAsSelector.Tag)
if err != nil {
return d, varWarning, err
}
}
// generic validation on devfile content
err = validate.ValidateDevfileData(d.Data)
if err != nil {

View File

@@ -1,5 +1,5 @@
//
// Copyright 2022 Red Hat, Inc.
// Copyright 2022-2023 Red Hat, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -69,6 +69,9 @@ func (d *DevfileCtx) SetDevfileContent() error {
if d.url != "" {
// set the client identifier for telemetry
params := util.HTTPRequestParams{URL: d.url, TelemetryClientName: util.TelemetryClientName}
if d.token != "" {
params.Token = d.token
}
data, err = util.DownloadInMemory(params)
if err != nil {
return errors.Wrap(err, "error getting devfile info from url")

View File

@@ -1,5 +1,5 @@
//
// Copyright 2022 Red Hat, Inc.
// Copyright 2022-2023 Red Hat, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -45,13 +45,16 @@ type DevfileCtx struct {
// devfile json schema
jsonSchema string
//url path of the devfile
// url path of the devfile
url string
// token is a personal access token used with a private git repo URL
token string
// filesystem for devfile
fs filesystem.Filesystem
// devfile kubernetes components has been coverted from uri to inlined in memory
// devfile kubernetes components has been converted from uri to inlined in memory
convertUriToInlined bool
}
@@ -150,6 +153,16 @@ func (d *DevfileCtx) GetURL() string {
return d.url
}
// GetToken func returns current devfile token
func (d *DevfileCtx) GetToken() string {
return d.token
}
// SetToken sets the token for the devfile
func (d *DevfileCtx) SetToken(token string) {
d.token = token
}
// SetAbsPath sets absolute file path for devfile
func (d *DevfileCtx) SetAbsPath() (err error) {
// Set devfile absolute path

View File

@@ -1,5 +1,5 @@
//
// Copyright 2022 Red Hat, Inc.
// Copyright 2022-2023 Red Hat, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -19,7 +19,7 @@ package version220
const JsonSchema220 = `{
"description": "Devfile describes the structure of a cloud-native devworkspace and development environment.",
"type": "object",
"title": "Devfile schema - Version 2.2.0-alpha",
"title": "Devfile schema - Version 2.2.1-alpha",
"required": [
"schemaVersion"
],
@@ -212,7 +212,7 @@ const JsonSchema220 = `{
"additionalProperties": false
},
"hotReloadCapable": {
"description": "Whether the command is capable to reload itself when source code changes. If set to 'true' the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is 'false'",
"description": "Specify whether the command is restarted or not when the source code changes. If set to 'true' the command won't be restarted. A *hotReloadCapable* 'run' or 'debug' command is expected to handle file changes on its own and won't be restarted. A *hotReloadCapable* 'build' command is expected to be executed only once and won't be executed again. This field is taken into account only for commands 'build', 'run' and 'debug' with 'isDefault' set to 'true'.\n\nDefault value is 'false'",
"type": "boolean"
},
"label": {
@@ -1104,7 +1104,7 @@ const JsonSchema220 = `{
"additionalProperties": false
},
"hotReloadCapable": {
"description": "Whether the command is capable to reload itself when source code changes. If set to 'true' the command won't be restarted and it is expected to handle file changes on its own.\n\nDefault value is 'false'",
"description": "Specify whether the command is restarted or not when the source code changes. If set to 'true' the command won't be restarted. A *hotReloadCapable* 'run' or 'debug' command is expected to handle file changes on its own and won't be restarted. A *hotReloadCapable* 'build' command is expected to be executed only once and won't be executed again. This field is taken into account only for commands 'build', 'run' and 'debug' with 'isDefault' set to 'true'.\n\nDefault value is 'false'",
"type": "boolean"
},
"label": {

View File

@@ -19,21 +19,21 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/devfile/library/v2/pkg/git"
"github.com/hashicorp/go-multierror"
"io/ioutil"
"net/url"
"os"
"path"
"reflect"
"strings"
"github.com/devfile/api/v2/pkg/attributes"
registryLibrary "github.com/devfile/registry-support/registry-library/library"
"reflect"
devfileCtx "github.com/devfile/library/v2/pkg/devfile/parser/context"
"github.com/devfile/library/v2/pkg/devfile/parser/data"
"github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common"
"github.com/devfile/library/v2/pkg/util"
registryLibrary "github.com/devfile/registry-support/registry-library/library"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/klog"
@@ -46,6 +46,57 @@ import (
"github.com/pkg/errors"
)
// downloadGitRepoResources is exposed as a global variable for the purpose of running mock tests
var downloadGitRepoResources = func(url string, destDir string, httpTimeout *int, token string) error {
var returnedErr error
gitUrl, err := git.NewGitUrlWithURL(url)
if err != nil {
return err
}
if gitUrl.IsGitProviderRepo() {
if !gitUrl.IsFile || gitUrl.Revision == "" || !strings.Contains(gitUrl.Path, OutputDevfileYamlPath) {
return fmt.Errorf("error getting devfile from url: failed to retrieve %s", url)
}
stackDir, err := os.MkdirTemp("", fmt.Sprintf("git-resources"))
if err != nil {
return fmt.Errorf("failed to create dir: %s, error: %v", stackDir, err)
}
defer func(path string) {
err := os.RemoveAll(path)
if err != nil {
returnedErr = multierror.Append(returnedErr, err)
}
}(stackDir)
if !gitUrl.IsPublic(httpTimeout) {
err = gitUrl.SetToken(token, httpTimeout)
if err != nil {
returnedErr = multierror.Append(returnedErr, err)
return returnedErr
}
}
err = gitUrl.CloneGitRepo(stackDir)
if err != nil {
returnedErr = multierror.Append(returnedErr, err)
return returnedErr
}
dir := path.Dir(path.Join(stackDir, gitUrl.Path))
err = git.CopyAllDirFiles(dir, destDir)
if err != nil {
returnedErr = multierror.Append(returnedErr, err)
return returnedErr
}
}
return nil
}
// ParseDevfile func validates the devfile integrity.
// Creates devfile context and runtime objects
func parseDevfile(d DevfileObj, resolveCtx *resolutionContextTree, tool resolverTools, flattenedDevfile bool) (DevfileObj, error) {
@@ -97,6 +148,8 @@ type ParserArgs struct {
// RegistryURLs is a list of registry hosts which parser should pull parent devfile from.
// If registryUrl is defined in devfile, this list will be ignored.
RegistryURLs []string
// Token is a GitHub, GitLab, or Bitbucket personal access token used with a private git repo URL
Token string
// DefaultNamespace is the default namespace to use
// If namespace is defined under devfile's parent kubernetes object, this namespace will be ignored.
DefaultNamespace string
@@ -111,11 +164,39 @@ type ParserArgs struct {
// SetBooleanDefaults sets the boolean properties to their default values after a devfile been parsed.
// The value is true by default. Clients can set this to false if they want to set the boolean properties themselves
SetBooleanDefaults *bool
// ImageNamesAsSelector sets the information that will be used to handle image names as selectors when parsing the Devfile.
// Not setting this field or setting it to nil disables the logic of handling image names as selectors.
ImageNamesAsSelector *ImageSelectorArgs
}
// ImageSelectorArgs defines the structure to leverage for using image names as selectors after parsing the Devfile.
// The fields defined here will be used together to compute the final image names that will be built and pushed,
// and replaced in all matching Image, Container or Kubernetes/OpenShift components.
//
// For Kubernetes/OpenShift components, replacement is done only in core Kubernetes resources
// (CronJob, DaemonSet, Deployment, Job, Pod, ReplicaSet, ReplicationController, StatefulSet) that are *inlined* in those components.
// Resources referenced via URIs will not be resolved. So you may want to also set ConvertKubernetesContentInUri to true in the parser args.
//
// For example, if Registry is set to "<local-registry>/<user-org>" and Tag is set to "some-dynamic-unique-tag",
// all container and Kubernetes/OpenShift components matching a relative image name (say "my-image-name") of an Image component
// will be replaced in the resulting Devfile by: "<local-registry>/<user-org>/<devfile-name>-my-image-name:some-dynamic-unique-tag".
type ImageSelectorArgs struct {
// Registry is the registry base path under which images matching selectors will be built and pushed to. Required.
//
// Example: <local-registry>/<user-org>
Registry string
// Tag represents a tag identifier under which images matching selectors will be built and pushed to.
// This should ideally be set to a unique identifier for each run of the caller tool.
Tag string
}
// ParseDevfile func populates the devfile data, parses and validates the devfile integrity.
// Creates devfile context and runtime objects
func ParseDevfile(args ParserArgs) (d DevfileObj, err error) {
if args.ImageNamesAsSelector != nil && strings.TrimSpace(args.ImageNamesAsSelector.Registry) == "" {
return DevfileObj{}, errors.New("registry is mandatory when setting ImageNamesAsSelector in the parser args")
}
if args.Data != nil {
d.Ctx, err = devfileCtx.NewByteContentDevfileCtx(args.Data)
if err != nil {
@@ -129,6 +210,10 @@ func ParseDevfile(args ParserArgs) (d DevfileObj, err error) {
return d, errors.Wrap(err, "the devfile source is not provided")
}
if args.Token != "" {
d.Ctx.SetToken(args.Token)
}
tool := resolverTools{
defaultNamespace: args.DefaultNamespace,
registryURLs: args.RegistryURLs,
@@ -431,17 +516,16 @@ func parseFromURI(importReference v1.ImportReference, curDevfileCtx devfileCtx.D
return DevfileObj{}, fmt.Errorf("failed to resolve parent uri, devfile context is missing absolute url and path to devfile. %s", resolveImportReference(importReference))
}
token := curDevfileCtx.GetToken()
d.Ctx = devfileCtx.NewURLDevfileCtx(newUri)
if strings.Contains(newUri, "raw.githubusercontent.com") {
urlComponents, err := util.GetGitUrlComponentsFromRaw(newUri)
if err != nil {
return DevfileObj{}, err
}
destDir := path.Dir(curDevfileCtx.GetAbsPath())
err = getResourcesFromGit(urlComponents, destDir)
if err != nil {
return DevfileObj{}, err
}
if token != "" {
d.Ctx.SetToken(token)
}
destDir := path.Dir(curDevfileCtx.GetAbsPath())
err = downloadGitRepoResources(newUri, destDir, tool.httpTimeout, token)
if err != nil {
return DevfileObj{}, err
}
}
importReference.Uri = newUri
@@ -450,27 +534,6 @@ func parseFromURI(importReference v1.ImportReference, curDevfileCtx devfileCtx.D
return populateAndParseDevfile(d, newResolveCtx, tool, true)
}
func getResourcesFromGit(gitUrlComponents map[string]string, destDir string) error {
stackDir, err := ioutil.TempDir(os.TempDir(), fmt.Sprintf("git-resources"))
if err != nil {
return fmt.Errorf("failed to create dir: %s, error: %v", stackDir, err)
}
defer os.RemoveAll(stackDir)
err = util.CloneGitRepo(gitUrlComponents, stackDir)
if err != nil {
return err
}
dir := path.Dir(path.Join(stackDir, gitUrlComponents["file"]))
err = util.CopyAllDirFiles(dir, destDir)
if err != nil {
return err
}
return nil
}
func parseFromRegistry(importReference v1.ImportReference, resolveCtx *resolutionContextTree, tool resolverTools) (d DevfileObj, err error) {
id := importReference.Id
registryURL := importReference.RegistryUrl
@@ -839,6 +902,9 @@ func getKubernetesDefinitionFromUri(uri string, d devfileCtx.DevfileCtx) ([]byte
newUri = uri
}
params := util.HTTPRequestParams{URL: newUri}
if d.GetToken() != "" {
params.Token = d.GetToken()
}
data, err = util.DownloadInMemory(params)
if err != nil {
return nil, errors.Wrapf(err, "error getting kubernetes resources definition information")

View File

@@ -1,5 +1,5 @@
//
// Copyright 2022 Red Hat, Inc.
// Copyright 2022-2023 Red Hat, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -121,7 +121,11 @@ func ParseKubernetesYaml(values []interface{}) (KubernetesResources, error) {
return KubernetesResources{}, err
}
kubernetesMap := value.(map[string]interface{})
var kubernetesMap map[string]interface{}
err = k8yaml.Unmarshal(byteData, &kubernetesMap)
if err != nil {
return KubernetesResources{}, err
}
kind := kubernetesMap["kind"]
switch kind {

372
vendor/github.com/devfile/library/v2/pkg/git/git.go generated vendored Normal file
View File

@@ -0,0 +1,372 @@
//
// Copyright 2023 Red Hat, 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 git
import (
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
)
const (
GitHubHost string = "github.com"
RawGitHubHost string = "raw.githubusercontent.com"
GitLabHost string = "gitlab.com"
BitbucketHost string = "bitbucket.org"
)
type GitUrl struct {
Protocol string // URL scheme
Host string // URL domain name
Owner string // name of the repo owner
Repo string // name of the repo
Revision string // branch name, tag name, or commit id
Path string // path to a directory or file in the repo
token string // authenticates private repo actions for parent devfiles
IsFile bool // defines if the URL points to a file in the repo
}
// NewGitUrlWithURL NewGitUrl creates a GitUrl from a string url
func NewGitUrlWithURL(url string) (GitUrl, error) {
gitUrl, err := ParseGitUrl(url)
if err != nil {
return gitUrl, err
}
return gitUrl, nil
}
// ParseGitUrl extracts information from a support git url
// Only supports git repositories hosted on GitHub, GitLab, and Bitbucket
func ParseGitUrl(fullUrl string) (GitUrl, error) {
var g GitUrl
err := ValidateURL(fullUrl)
if err != nil {
return g, err
}
parsedUrl, err := url.Parse(fullUrl)
if err != nil {
return g, err
}
if len(parsedUrl.Path) == 0 {
return g, fmt.Errorf("url path should not be empty")
}
if parsedUrl.Host == RawGitHubHost || parsedUrl.Host == GitHubHost {
err = g.parseGitHubUrl(parsedUrl)
} else if parsedUrl.Host == GitLabHost {
err = g.parseGitLabUrl(parsedUrl)
} else if parsedUrl.Host == BitbucketHost {
err = g.parseBitbucketUrl(parsedUrl)
} else {
err = fmt.Errorf("url host should be a valid GitHub, GitLab, or Bitbucket host; received: %s", parsedUrl.Host)
}
return g, err
}
func (g *GitUrl) GetToken() string {
return g.token
}
type CommandType string
const (
GitCommand CommandType = "git"
unsupportedCmdMsg = "Unsupported command \"%s\" "
)
// Execute is exposed as a global variable for the purpose of running mock tests
// only "git" is supported
/* #nosec G204 -- used internally to execute various git actions and eventual cleanup of artifacts. Calling methods validate user input to ensure commands are used appropriately */
var execute = func(baseDir string, cmd CommandType, args ...string) ([]byte, error) {
if cmd == GitCommand {
c := exec.Command(string(cmd), args...)
c.Dir = baseDir
output, err := c.CombinedOutput()
return output, err
}
return []byte(""), fmt.Errorf(unsupportedCmdMsg, string(cmd))
}
func (g *GitUrl) CloneGitRepo(destDir string) error {
exist := CheckPathExists(destDir)
if !exist {
return fmt.Errorf("failed to clone repo, destination directory: '%s' does not exists", destDir)
}
host := g.Host
if host == RawGitHubHost {
host = GitHubHost
}
var repoUrl string
if g.GetToken() == "" {
repoUrl = fmt.Sprintf("%s://%s/%s/%s.git", g.Protocol, host, g.Owner, g.Repo)
} else {
repoUrl = fmt.Sprintf("%s://token:%s@%s/%s/%s.git", g.Protocol, g.GetToken(), host, g.Owner, g.Repo)
if g.Host == BitbucketHost {
repoUrl = fmt.Sprintf("%s://x-token-auth:%s@%s/%s/%s.git", g.Protocol, g.GetToken(), host, g.Owner, g.Repo)
}
}
_, err := execute(destDir, "git", "clone", repoUrl, destDir)
if err != nil {
if g.GetToken() == "" {
return fmt.Errorf("failed to clone repo without a token, ensure that a token is set if the repo is private. error: %v", err)
} else {
return fmt.Errorf("failed to clone repo with token, ensure that the url and token is correct. error: %v", err)
}
}
if g.Revision != "" {
_, err := execute(destDir, "git", "switch", "--detach", "origin/"+g.Revision)
if err != nil {
err = os.RemoveAll(destDir)
if err != nil {
return err
}
return fmt.Errorf("failed to switch repo to revision. repo dir: %v, revision: %v", destDir, g.Revision)
}
}
return nil
}
func (g *GitUrl) parseGitHubUrl(url *url.URL) error {
var splitUrl []string
var err error
g.Protocol = url.Scheme
g.Host = url.Host
if g.Host == RawGitHubHost {
g.IsFile = true
// raw GitHub urls don't contain "blob" or "tree"
// https://raw.githubusercontent.com/devfile/library/main/devfile.yaml -> [devfile library main devfile.yaml]
splitUrl = strings.SplitN(url.Path[1:], "/", 4)
if len(splitUrl) == 4 {
g.Owner = splitUrl[0]
g.Repo = splitUrl[1]
g.Revision = splitUrl[2]
g.Path = splitUrl[3]
} else {
// raw GitHub urls have to be a file
err = fmt.Errorf("raw url path should contain <owner>/<repo>/<branch>/<path/to/file>, received: %s", url.Path[1:])
}
return err
}
if g.Host == GitHubHost {
// https://github.com/devfile/library/blob/main/devfile.yaml -> [devfile library blob main devfile.yaml]
splitUrl = strings.SplitN(url.Path[1:], "/", 5)
if len(splitUrl) < 2 {
err = fmt.Errorf("url path should contain <user>/<repo>, received: %s", url.Path[1:])
} else {
g.Owner = splitUrl[0]
g.Repo = splitUrl[1]
// url doesn't contain a path to a directory or file
if len(splitUrl) == 2 {
return nil
}
switch splitUrl[2] {
case "tree":
g.IsFile = false
case "blob":
g.IsFile = true
default:
return fmt.Errorf("url path to directory or file should contain 'tree' or 'blob'")
}
// url has a path to a file or directory
if len(splitUrl) == 5 {
g.Revision = splitUrl[3]
g.Path = splitUrl[4]
} else if !g.IsFile && len(splitUrl) == 4 {
g.Revision = splitUrl[3]
} else {
err = fmt.Errorf("url path should contain <owner>/<repo>/<tree or blob>/<branch>/<path/to/file/or/directory>, received: %s", url.Path[1:])
}
}
}
return err
}
func (g *GitUrl) parseGitLabUrl(url *url.URL) error {
var splitFile, splitOrg []string
var err error
g.Protocol = url.Scheme
g.Host = url.Host
g.IsFile = false
// GitLab urls contain a '-' separating the root of the repo
// and the path to a file or directory
split := strings.Split(url.Path[1:], "/-/")
splitOrg = strings.SplitN(split[0], "/", 2)
if len(splitOrg) < 2 {
return fmt.Errorf("url path should contain <user>/<repo>, received: %s", url.Path[1:])
} else {
g.Owner = splitOrg[0]
g.Repo = splitOrg[1]
}
// url doesn't contain a path to a directory or file
if len(split) == 1 {
return nil
}
// url may contain a path to a directory or file
if len(split) == 2 {
splitFile = strings.SplitN(split[1], "/", 3)
}
if len(splitFile) == 3 {
if splitFile[0] == "blob" || splitFile[0] == "tree" || splitFile[0] == "raw" {
g.Revision = splitFile[1]
g.Path = splitFile[2]
ext := filepath.Ext(g.Path)
if ext != "" {
g.IsFile = true
}
} else {
err = fmt.Errorf("url path should contain 'blob' or 'tree' or 'raw', received: %s", url.Path[1:])
}
} else {
return fmt.Errorf("url path to directory or file should contain <blob or tree or raw>/<branch>/<path/to/file/or/directory>, received: %s", url.Path[1:])
}
return err
}
func (g *GitUrl) parseBitbucketUrl(url *url.URL) error {
var splitUrl []string
var err error
g.Protocol = url.Scheme
g.Host = url.Host
g.IsFile = false
splitUrl = strings.SplitN(url.Path[1:], "/", 5)
if len(splitUrl) < 2 {
err = fmt.Errorf("url path should contain <user>/<repo>, received: %s", url.Path[1:])
} else if len(splitUrl) == 2 {
g.Owner = splitUrl[0]
g.Repo = splitUrl[1]
} else {
g.Owner = splitUrl[0]
g.Repo = splitUrl[1]
if len(splitUrl) == 5 {
if splitUrl[2] == "raw" || splitUrl[2] == "src" {
g.Revision = splitUrl[3]
g.Path = splitUrl[4]
ext := filepath.Ext(g.Path)
if ext != "" {
g.IsFile = true
}
} else {
err = fmt.Errorf("url path should contain 'raw' or 'src', received: %s", url.Path[1:])
}
} else {
err = fmt.Errorf("url path should contain path to directory or file, received: %s", url.Path[1:])
}
}
return err
}
// SetToken validates the token with a get request to the repo before setting the token
// Defaults token to empty on failure.
func (g *GitUrl) SetToken(token string, httpTimeout *int) error {
err := g.validateToken(HTTPRequestParams{Token: token, Timeout: httpTimeout})
if err != nil {
g.token = ""
return fmt.Errorf("failed to set token. error: %v", err)
}
g.token = token
return nil
}
// IsPublic checks if the GitUrl is public with a get request to the repo using an empty token
// Returns true if the request succeeds
func (g *GitUrl) IsPublic(httpTimeout *int) bool {
err := g.validateToken(HTTPRequestParams{Token: "", Timeout: httpTimeout})
if err != nil {
return false
}
return true
}
// validateToken makes a http get request to the repo with the GitUrl token
// Returns an error if the get request fails
func (g *GitUrl) validateToken(params HTTPRequestParams) error {
var apiUrl string
switch g.Host {
case GitHubHost, RawGitHubHost:
apiUrl = fmt.Sprintf("https://api.github.com/repos/%s/%s", g.Owner, g.Repo)
case GitLabHost:
apiUrl = fmt.Sprintf("https://gitlab.com/api/v4/projects/%s%%2F%s", g.Owner, g.Repo)
case BitbucketHost:
apiUrl = fmt.Sprintf("https://api.bitbucket.org/2.0/repositories/%s/%s", g.Owner, g.Repo)
default:
apiUrl = fmt.Sprintf("%s://%s/%s/%s.git", g.Protocol, g.Host, g.Owner, g.Repo)
}
params.URL = apiUrl
res, err := HTTPGetRequest(params, 0)
if len(res) == 0 || err != nil {
return err
}
return nil
}
// GitRawFileAPI returns the endpoint for the git providers raw file
func (g *GitUrl) GitRawFileAPI() string {
var apiRawFile string
switch g.Host {
case GitHubHost, RawGitHubHost:
apiRawFile = fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s", g.Owner, g.Repo, g.Revision, g.Path)
case GitLabHost:
apiRawFile = fmt.Sprintf("https://gitlab.com/api/v4/projects/%s%%2F%s/repository/files/%s/raw?ref=%s", g.Owner, g.Repo, g.Path, g.Revision)
case BitbucketHost:
apiRawFile = fmt.Sprintf("https://api.bitbucket.org/2.0/repositories/%s/%s/src/%s/%s", g.Owner, g.Repo, g.Revision, g.Path)
}
return apiRawFile
}
// IsGitProviderRepo checks if the url matches a repo from a supported git provider
func (g *GitUrl) IsGitProviderRepo() bool {
switch g.Host {
case GitHubHost, RawGitHubHost, GitLabHost, BitbucketHost:
return true
default:
return false
}
}

146
vendor/github.com/devfile/library/v2/pkg/git/mock.go generated vendored Normal file
View File

@@ -0,0 +1,146 @@
//
// Copyright 2023 Red Hat, 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 git
import (
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
)
type MockGitUrl struct {
Protocol string // URL scheme
Host string // URL domain name
Owner string // name of the repo owner
Repo string // name of the repo
Revision string // branch name, tag name, or commit id
Path string // path to a directory or file in the repo
token string // used for authenticating a private repo
IsFile bool // defines if the URL points to a file in the repo
}
func (m *MockGitUrl) GetToken() string {
return m.token
}
var mockExecute = func(baseDir string, cmd CommandType, args ...string) ([]byte, error) {
if cmd == GitCommand {
if len(args) > 0 && args[0] == "clone" {
u, _ := url.Parse(args[1])
password, hasPassword := u.User.Password()
resourceFile, err := os.Create(filepath.Clean(baseDir) + "/resource.file")
if err != nil {
return nil, fmt.Errorf("failed to create test resource: %v", err)
}
// private repository
if hasPassword {
switch password {
case "valid-token":
_, err := resourceFile.WriteString("private repo\n")
if err != nil {
return nil, fmt.Errorf("failed to write to test resource: %v", err)
}
return []byte(""), nil
default:
return []byte(""), fmt.Errorf("not a valid token")
}
}
_, err = resourceFile.WriteString("public repo\n")
if err != nil {
return nil, fmt.Errorf("failed to write to test resource: %v", err)
}
return []byte(""), nil
}
if len(args) > 0 && args[0] == "switch" {
revision := strings.TrimPrefix(args[2], "origin/")
if revision != "invalid-revision" {
resourceFile, err := os.OpenFile(filepath.Clean(baseDir)+"/resource.file", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return nil, fmt.Errorf("failed to open test resource: %v", err)
}
_, err = resourceFile.WriteString("git switched")
if err != nil {
return nil, fmt.Errorf("failed to write to test resource: %v", err)
}
return []byte("git switched to revision"), nil
}
return []byte(""), fmt.Errorf("failed to switch revision")
}
}
return []byte(""), fmt.Errorf(unsupportedCmdMsg, string(cmd))
}
func (m *MockGitUrl) CloneGitRepo(destDir string) error {
exist := CheckPathExists(destDir)
if !exist {
return fmt.Errorf("failed to clone repo, destination directory: '%s' does not exists", destDir)
}
host := m.Host
if host == RawGitHubHost {
host = GitHubHost
}
var repoUrl string
if m.GetToken() == "" {
repoUrl = fmt.Sprintf("%s://%s/%s/%s.git", m.Protocol, host, m.Owner, m.Repo)
} else {
repoUrl = fmt.Sprintf("%s://token:%s@%s/%s/%s.git", m.Protocol, m.GetToken(), host, m.Owner, m.Repo)
if m.Host == BitbucketHost {
repoUrl = fmt.Sprintf("%s://x-token-auth:%s@%s/%s/%s.git", m.Protocol, m.GetToken(), host, m.Owner, m.Repo)
}
}
_, err := mockExecute(destDir, "git", "clone", repoUrl, destDir)
if err != nil {
if m.GetToken() == "" {
return fmt.Errorf("failed to clone repo without a token, ensure that a token is set if the repo is private")
} else {
return fmt.Errorf("failed to clone repo with token, ensure that the url and token is correct")
}
}
if m.Revision != "" {
_, err := mockExecute(destDir, "git", "switch", "--detach", "origin/"+m.Revision)
if err != nil {
return fmt.Errorf("failed to switch repo to revision. repo dir: %v, revision: %v", destDir, m.Revision)
}
}
return nil
}
func (m *MockGitUrl) SetToken(token string) error {
m.token = token
return nil
}
func (m *MockGitUrl) IsGitProviderRepo() bool {
switch m.Host {
case GitHubHost, RawGitHubHost, GitLabHost, BitbucketHost:
return true
default:
return false
}
}

260
vendor/github.com/devfile/library/v2/pkg/git/util.go generated vendored Normal file
View File

@@ -0,0 +1,260 @@
//
// Copyright 2023 Red Hat, 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 git
import (
"fmt"
"github.com/devfile/library/v2/pkg/testingutil/filesystem"
"github.com/gregjones/httpcache"
"github.com/gregjones/httpcache/diskcache"
"github.com/pkg/errors"
"io"
"io/ioutil"
"k8s.io/klog"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"time"
)
const (
HTTPRequestResponseTimeout = 30 * time.Second // HTTPRequestTimeout configures timeout of all HTTP requests
)
// httpCacheDir determines directory where odo will cache HTTP responses
var httpCacheDir = filepath.Join(os.TempDir(), "odohttpcache")
// HTTPRequestParams holds parameters of forming http request
type HTTPRequestParams struct {
URL string
Token string
Timeout *int
TelemetryClientName string //optional client name for telemetry
}
// HTTPGetRequest gets resource contents given URL and token (if applicable)
// cacheFor determines how long the response should be cached (in minutes), 0 for no caching
func HTTPGetRequest(request HTTPRequestParams, cacheFor int) ([]byte, error) {
// Build http request
req, err := http.NewRequest("GET", request.URL, nil)
if err != nil {
return nil, err
}
if request.Token != "" {
bearer := "Bearer " + request.Token
req.Header.Add("Authorization", bearer)
}
//add the telemetry client name
req.Header.Add("Client", request.TelemetryClientName)
overriddenTimeout := HTTPRequestResponseTimeout
timeout := request.Timeout
if timeout != nil {
//if value is invalid, the default will be used
if *timeout > 0 {
//convert timeout to seconds
overriddenTimeout = time.Duration(*timeout) * time.Second
klog.V(4).Infof("HTTP request and response timeout overridden value is %v ", overriddenTimeout)
} else {
klog.V(4).Infof("Invalid httpTimeout is passed in, using default value")
}
}
httpClient := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
ResponseHeaderTimeout: overriddenTimeout,
},
Timeout: overriddenTimeout,
}
klog.V(4).Infof("HTTPGetRequest: %s", req.URL.String())
if cacheFor > 0 {
// if there is an error during cache setup we show warning and continue without using cache
cacheError := false
httpCacheTime := time.Duration(cacheFor) * time.Minute
// make sure that cache directory exists
err = os.MkdirAll(httpCacheDir, 0750)
if err != nil {
cacheError = true
klog.WarningDepth(4, "Unable to setup cache: ", err)
}
err = cleanHttpCache(httpCacheDir, httpCacheTime)
if err != nil {
cacheError = true
klog.WarningDepth(4, "Unable to clean up cache directory: ", err)
}
if !cacheError {
httpClient.Transport = httpcache.NewTransport(diskcache.New(httpCacheDir))
klog.V(4).Infof("Response will be cached in %s for %s", httpCacheDir, httpCacheTime)
} else {
klog.V(4).Info("Response won't be cached.")
}
}
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.Header.Get(httpcache.XFromCache) != "" {
klog.V(4).Infof("Cached response used.")
}
// We have a non 1xx / 2xx status, return an error
if (resp.StatusCode - 300) > 0 {
return nil, errors.Errorf("failed to retrieve %s, %v: %s", request.URL, resp.StatusCode, http.StatusText(resp.StatusCode))
}
// Process http response
bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return bytes, err
}
// ValidateURL validates the URL
func ValidateURL(sourceURL string) error {
u, err := url.Parse(sourceURL)
if err != nil {
return err
}
if len(u.Host) == 0 || len(u.Scheme) == 0 {
return errors.New("URL is invalid")
}
return nil
}
// cleanHttpCache checks cacheDir and deletes all files that were modified more than cacheTime back
func cleanHttpCache(cacheDir string, cacheTime time.Duration) error {
cacheFiles, err := ioutil.ReadDir(cacheDir)
if err != nil {
return err
}
for _, f := range cacheFiles {
if f.ModTime().Add(cacheTime).Before(time.Now()) {
klog.V(4).Infof("Removing cache file %s, because it is older than %s", f.Name(), cacheTime.String())
err := os.Remove(filepath.Join(cacheDir, f.Name()))
if err != nil {
return err
}
}
}
return nil
}
// CheckPathExists checks if a path exists or not
func CheckPathExists(path string) bool {
return checkPathExistsOnFS(path, filesystem.DefaultFs{})
}
func checkPathExistsOnFS(path string, fs filesystem.Filesystem) bool {
if _, err := fs.Stat(path); !os.IsNotExist(err) {
// path to file does exist
return true
}
klog.V(4).Infof("path %s doesn't exist, skipping it", path)
return false
}
// CopyAllDirFiles recursively copies a source directory to a destination directory
func CopyAllDirFiles(srcDir, destDir string) error {
return copyAllDirFilesOnFS(srcDir, destDir, filesystem.DefaultFs{})
}
func copyAllDirFilesOnFS(srcDir, destDir string, fs filesystem.Filesystem) error {
var info os.FileInfo
files, err := fs.ReadDir(srcDir)
if err != nil {
return errors.Wrapf(err, "failed reading dir %v", srcDir)
}
for _, file := range files {
srcPath := path.Join(srcDir, file.Name())
destPath := path.Join(destDir, file.Name())
if file.IsDir() {
if info, err = fs.Stat(srcPath); err != nil {
return err
}
if err = fs.MkdirAll(destPath, info.Mode()); err != nil {
return err
}
if err = copyAllDirFilesOnFS(srcPath, destPath, fs); err != nil {
return err
}
} else {
if file.Name() == "devfile.yaml" {
continue
}
// Only copy files that do not exist in the destination directory
if !checkPathExistsOnFS(destPath, fs) {
if err := copyFileOnFs(srcPath, destPath, fs); err != nil {
return errors.Wrapf(err, "failed to copy %s to %s", srcPath, destPath)
}
}
}
}
return nil
}
// copied from: https://github.com/devfile/registry-support/blob/main/index/generator/library/util.go
func copyFileOnFs(src, dst string, fs filesystem.Filesystem) error {
var err error
var srcinfo os.FileInfo
srcfd, err := fs.Open(src)
if err != nil {
return err
}
defer func() {
if e := srcfd.Close(); e != nil {
fmt.Printf("err occurred while closing file: %v", e)
}
}()
dstfd, err := fs.Create(dst)
if err != nil {
return err
}
defer func() {
if e := dstfd.Close(); e != nil {
fmt.Printf("err occurred while closing file: %v", e)
}
}()
if _, err = io.Copy(dstfd, srcfd); err != nil {
return err
}
if srcinfo, err = fs.Stat(src); err != nil {
return err
}
return fs.Chmod(dst, srcinfo.Mode())
}

View File

@@ -1,5 +1,5 @@
//
// Copyright 2022 Red Hat, Inc.
// Copyright 2022-2023 Red Hat, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -21,6 +21,9 @@ import (
"bytes"
"crypto/rand"
"fmt"
"github.com/devfile/library/v2/pkg/git"
gitpkg "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"io"
"io/ioutil"
"math/big"
@@ -41,11 +44,8 @@ import (
"syscall"
"time"
"github.com/go-git/go-git/v5/plumbing"
"github.com/devfile/library/v2/pkg/testingutil/filesystem"
"github.com/fatih/color"
gitpkg "github.com/go-git/go-git/v5"
"github.com/gobwas/glob"
"github.com/gregjones/httpcache"
"github.com/gregjones/httpcache/diskcache"
@@ -57,6 +57,10 @@ import (
"k8s.io/klog"
)
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
const (
HTTPRequestResponseTimeout = 30 * time.Second // HTTPRequestTimeout configures timeout of all HTTP requests
ModeReadWriteFile = 0600 // default Permission for a file
@@ -881,6 +885,15 @@ func ConvertGitSSHRemoteToHTTPS(remote string) string {
return remote
}
// IsGitProviderRepo checks if the url matches a repo from a supported git provider
func IsGitProviderRepo(url string) bool {
if strings.Contains(url, git.RawGitHubHost) || strings.Contains(url, git.GitHubHost) ||
strings.Contains(url, git.GitLabHost) || strings.Contains(url, git.BitbucketHost) {
return true
}
return false
}
// GetAndExtractZip downloads a zip file from a URL with a http prefix or
// takes an absolute path prefixed with file:// and extracts it to a destination.
// pathToUnzip specifies the path within the zip folder to extract
@@ -1083,17 +1096,47 @@ func DownloadFileInMemory(url string) ([]byte, error) {
// DownloadInMemory uses HTTPRequestParams to download the file and return bytes
func DownloadInMemory(params HTTPRequestParams) ([]byte, error) {
var httpClient = &http.Client{Transport: &http.Transport{
ResponseHeaderTimeout: HTTPRequestResponseTimeout,
}, Timeout: HTTPRequestResponseTimeout}
url := params.URL
var g git.GitUrl
var err error
if IsGitProviderRepo(params.URL) {
g, err = git.NewGitUrlWithURL(params.URL)
if err != nil {
return nil, errors.Errorf("failed to parse git repo. error: %v", err)
}
}
return downloadInMemoryWithClient(params, httpClient, g)
}
func downloadInMemoryWithClient(params HTTPRequestParams, httpClient HTTPClient, g git.GitUrl) ([]byte, error) {
var url string
url = params.URL
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
if IsGitProviderRepo(url) {
url = g.GitRawFileAPI()
req, err = http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
if !g.IsPublic(params.Timeout) {
// check that the token is valid before adding to the header
err = g.SetToken(params.Token, params.Timeout)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", params.Token))
}
}
//add the telemetry client name in the header
req.Header.Add("Client", params.TelemetryClientName)
resp, err := httpClient.Do(req)
@@ -1187,6 +1230,7 @@ func ValidateFile(filePath string) error {
}
// GetGitUrlComponentsFromRaw converts a raw GitHub file link to a map of the url components
// Deprecated: in favor of the method git.ParseGitUrl() with the devfile/library/v2/pkg/git package
func GetGitUrlComponentsFromRaw(rawGitURL string) (map[string]string, error) {
var urlComponents map[string]string
@@ -1219,6 +1263,7 @@ func GetGitUrlComponentsFromRaw(rawGitURL string) (map[string]string, error) {
}
// CloneGitRepo clones a GitHub repo to a destination directory
// Deprecated: in favor of the method git.CloneGitRepo() with the devfile/library/v2/pkg/git package
func CloneGitRepo(gitUrlComponents map[string]string, destDir string) error {
gitUrl := fmt.Sprintf("https://github.com/%s/%s.git", gitUrlComponents["username"], gitUrlComponents["project"])
branch := fmt.Sprintf("refs/heads/%s", gitUrlComponents["branch"])

202
vendor/github.com/distribution/distribution/v3/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
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.

View File

@@ -0,0 +1,247 @@
package digestset
import (
"errors"
"sort"
"strings"
"sync"
digest "github.com/opencontainers/go-digest"
)
var (
// ErrDigestNotFound is used when a matching digest
// could not be found in a set.
ErrDigestNotFound = errors.New("digest not found")
// ErrDigestAmbiguous is used when multiple digests
// are found in a set. None of the matching digests
// should be considered valid matches.
ErrDigestAmbiguous = errors.New("ambiguous digest string")
)
// Set is used to hold a unique set of digests which
// may be easily referenced by easily referenced by a string
// representation of the digest as well as short representation.
// The uniqueness of the short representation is based on other
// digests in the set. If digests are omitted from this set,
// collisions in a larger set may not be detected, therefore it
// is important to always do short representation lookups on
// the complete set of digests. To mitigate collisions, an
// appropriately long short code should be used.
type Set struct {
mutex sync.RWMutex
entries digestEntries
}
// NewSet creates an empty set of digests
// which may have digests added.
func NewSet() *Set {
return &Set{
entries: digestEntries{},
}
}
// checkShortMatch checks whether two digests match as either whole
// values or short values. This function does not test equality,
// rather whether the second value could match against the first
// value.
func checkShortMatch(alg digest.Algorithm, hex, shortAlg, shortHex string) bool {
if len(hex) == len(shortHex) {
if hex != shortHex {
return false
}
if len(shortAlg) > 0 && string(alg) != shortAlg {
return false
}
} else if !strings.HasPrefix(hex, shortHex) {
return false
} else if len(shortAlg) > 0 && string(alg) != shortAlg {
return false
}
return true
}
// Lookup looks for a digest matching the given string representation.
// If no digests could be found ErrDigestNotFound will be returned
// with an empty digest value. If multiple matches are found
// ErrDigestAmbiguous will be returned with an empty digest value.
func (dst *Set) Lookup(d string) (digest.Digest, error) {
dst.mutex.RLock()
defer dst.mutex.RUnlock()
if len(dst.entries) == 0 {
return "", ErrDigestNotFound
}
var (
searchFunc func(int) bool
alg digest.Algorithm
hex string
)
dgst, err := digest.Parse(d)
if err == digest.ErrDigestInvalidFormat {
hex = d
searchFunc = func(i int) bool {
return dst.entries[i].val >= d
}
} else {
hex = dgst.Hex()
alg = dgst.Algorithm()
searchFunc = func(i int) bool {
if dst.entries[i].val == hex {
return dst.entries[i].alg >= alg
}
return dst.entries[i].val >= hex
}
}
idx := sort.Search(len(dst.entries), searchFunc)
if idx == len(dst.entries) || !checkShortMatch(dst.entries[idx].alg, dst.entries[idx].val, string(alg), hex) {
return "", ErrDigestNotFound
}
if dst.entries[idx].alg == alg && dst.entries[idx].val == hex {
return dst.entries[idx].digest, nil
}
if idx+1 < len(dst.entries) && checkShortMatch(dst.entries[idx+1].alg, dst.entries[idx+1].val, string(alg), hex) {
return "", ErrDigestAmbiguous
}
return dst.entries[idx].digest, nil
}
// Add adds the given digest to the set. An error will be returned
// if the given digest is invalid. If the digest already exists in the
// set, this operation will be a no-op.
func (dst *Set) Add(d digest.Digest) error {
if err := d.Validate(); err != nil {
return err
}
dst.mutex.Lock()
defer dst.mutex.Unlock()
entry := &digestEntry{alg: d.Algorithm(), val: d.Hex(), digest: d}
searchFunc := func(i int) bool {
if dst.entries[i].val == entry.val {
return dst.entries[i].alg >= entry.alg
}
return dst.entries[i].val >= entry.val
}
idx := sort.Search(len(dst.entries), searchFunc)
if idx == len(dst.entries) {
dst.entries = append(dst.entries, entry)
return nil
} else if dst.entries[idx].digest == d {
return nil
}
entries := append(dst.entries, nil)
copy(entries[idx+1:], entries[idx:len(entries)-1])
entries[idx] = entry
dst.entries = entries
return nil
}
// Remove removes the given digest from the set. An err will be
// returned if the given digest is invalid. If the digest does
// not exist in the set, this operation will be a no-op.
func (dst *Set) Remove(d digest.Digest) error {
if err := d.Validate(); err != nil {
return err
}
dst.mutex.Lock()
defer dst.mutex.Unlock()
entry := &digestEntry{alg: d.Algorithm(), val: d.Hex(), digest: d}
searchFunc := func(i int) bool {
if dst.entries[i].val == entry.val {
return dst.entries[i].alg >= entry.alg
}
return dst.entries[i].val >= entry.val
}
idx := sort.Search(len(dst.entries), searchFunc)
// Not found if idx is after or value at idx is not digest
if idx == len(dst.entries) || dst.entries[idx].digest != d {
return nil
}
entries := dst.entries
copy(entries[idx:], entries[idx+1:])
entries = entries[:len(entries)-1]
dst.entries = entries
return nil
}
// All returns all the digests in the set
func (dst *Set) All() []digest.Digest {
dst.mutex.RLock()
defer dst.mutex.RUnlock()
retValues := make([]digest.Digest, len(dst.entries))
for i := range dst.entries {
retValues[i] = dst.entries[i].digest
}
return retValues
}
// ShortCodeTable returns a map of Digest to unique short codes. The
// length represents the minimum value, the maximum length may be the
// entire value of digest if uniqueness cannot be achieved without the
// full value. This function will attempt to make short codes as short
// as possible to be unique.
func ShortCodeTable(dst *Set, length int) map[digest.Digest]string {
dst.mutex.RLock()
defer dst.mutex.RUnlock()
m := make(map[digest.Digest]string, len(dst.entries))
l := length
resetIdx := 0
for i := 0; i < len(dst.entries); i++ {
var short string
extended := true
for extended {
extended = false
if len(dst.entries[i].val) <= l {
short = dst.entries[i].digest.String()
} else {
short = dst.entries[i].val[:l]
for j := i + 1; j < len(dst.entries); j++ {
if checkShortMatch(dst.entries[j].alg, dst.entries[j].val, "", short) {
if j > resetIdx {
resetIdx = j
}
extended = true
} else {
break
}
}
if extended {
l++
}
}
}
m[dst.entries[i].digest] = short
if i >= resetIdx {
l = length
}
}
return m
}
type digestEntry struct {
alg digest.Algorithm
val string
digest digest.Digest
}
type digestEntries []*digestEntry
func (d digestEntries) Len() int {
return len(d)
}
func (d digestEntries) Less(i, j int) bool {
if d[i].val != d[j].val {
return d[i].val < d[j].val
}
return d[i].alg < d[j].alg
}
func (d digestEntries) Swap(i, j int) {
d[i], d[j] = d[j], d[i]
}

View File

@@ -0,0 +1,42 @@
package reference
import "path"
// IsNameOnly returns true if reference only contains a repo name.
func IsNameOnly(ref Named) bool {
if _, ok := ref.(NamedTagged); ok {
return false
}
if _, ok := ref.(Canonical); ok {
return false
}
return true
}
// FamiliarName returns the familiar name string
// for the given named, familiarizing if needed.
func FamiliarName(ref Named) string {
if nn, ok := ref.(normalizedNamed); ok {
return nn.Familiar().Name()
}
return ref.Name()
}
// FamiliarString returns the familiar string representation
// for the given reference, familiarizing if needed.
func FamiliarString(ref Reference) string {
if nn, ok := ref.(normalizedNamed); ok {
return nn.Familiar().String()
}
return ref.String()
}
// FamiliarMatch reports whether ref matches the specified pattern.
// See https://godoc.org/path#Match for supported patterns.
func FamiliarMatch(pattern string, ref Reference) (bool, error) {
matched, err := path.Match(pattern, FamiliarString(ref))
if namedRef, isNamed := ref.(Named); isNamed && !matched {
matched, _ = path.Match(pattern, FamiliarName(namedRef))
}
return matched, err
}

View File

@@ -0,0 +1,198 @@
package reference
import (
"fmt"
"strings"
"github.com/distribution/distribution/v3/digestset"
"github.com/opencontainers/go-digest"
)
var (
legacyDefaultDomain = "index.docker.io"
defaultDomain = "docker.io"
officialRepoName = "library"
defaultTag = "latest"
)
// normalizedNamed represents a name which has been
// normalized and has a familiar form. A familiar name
// is what is used in Docker UI. An example normalized
// name is "docker.io/library/ubuntu" and corresponding
// familiar name of "ubuntu".
type normalizedNamed interface {
Named
Familiar() Named
}
// ParseNormalizedNamed parses a string into a named reference
// transforming a familiar name from Docker UI to a fully
// qualified reference. If the value may be an identifier
// use ParseAnyReference.
func ParseNormalizedNamed(s string) (Named, error) {
if ok := anchoredIdentifierRegexp.MatchString(s); ok {
return nil, fmt.Errorf("invalid repository name (%s), cannot specify 64-byte hexadecimal strings", s)
}
domain, remainder := splitDockerDomain(s)
var remoteName string
if tagSep := strings.IndexRune(remainder, ':'); tagSep > -1 {
remoteName = remainder[:tagSep]
} else {
remoteName = remainder
}
if strings.ToLower(remoteName) != remoteName {
return nil, fmt.Errorf("invalid reference format: repository name (%s) must be lowercase", remoteName)
}
ref, err := Parse(domain + "/" + remainder)
if err != nil {
return nil, err
}
named, isNamed := ref.(Named)
if !isNamed {
return nil, fmt.Errorf("reference %s has no name", ref.String())
}
return named, nil
}
// ParseDockerRef normalizes the image reference following the docker convention. This is added
// mainly for backward compatibility.
// The reference returned can only be either tagged or digested. For reference contains both tag
// and digest, the function returns digested reference, e.g. docker.io/library/busybox:latest@
// sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa will be returned as
// docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa.
func ParseDockerRef(ref string) (Named, error) {
named, err := ParseNormalizedNamed(ref)
if err != nil {
return nil, err
}
if _, ok := named.(NamedTagged); ok {
if canonical, ok := named.(Canonical); ok {
// The reference is both tagged and digested, only
// return digested.
newNamed, err := WithName(canonical.Name())
if err != nil {
return nil, err
}
newCanonical, err := WithDigest(newNamed, canonical.Digest())
if err != nil {
return nil, err
}
return newCanonical, nil
}
}
return TagNameOnly(named), nil
}
// splitDockerDomain splits a repository name to domain and remotename string.
// If no valid domain is found, the default domain is used. Repository name
// needs to be already validated before.
func splitDockerDomain(name string) (domain, remainder string) {
i := strings.IndexRune(name, '/')
if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost" && strings.ToLower(name[:i]) == name[:i]) {
domain, remainder = defaultDomain, name
} else {
domain, remainder = name[:i], name[i+1:]
}
if domain == legacyDefaultDomain {
domain = defaultDomain
}
if domain == defaultDomain && !strings.ContainsRune(remainder, '/') {
remainder = officialRepoName + "/" + remainder
}
return
}
// familiarizeName returns a shortened version of the name familiar
// to to the Docker UI. Familiar names have the default domain
// "docker.io" and "library/" repository prefix removed.
// For example, "docker.io/library/redis" will have the familiar
// name "redis" and "docker.io/dmcgowan/myapp" will be "dmcgowan/myapp".
// Returns a familiarized named only reference.
func familiarizeName(named namedRepository) repository {
repo := repository{
domain: named.Domain(),
path: named.Path(),
}
if repo.domain == defaultDomain {
repo.domain = ""
// Handle official repositories which have the pattern "library/<official repo name>"
if split := strings.Split(repo.path, "/"); len(split) == 2 && split[0] == officialRepoName {
repo.path = split[1]
}
}
return repo
}
func (r reference) Familiar() Named {
return reference{
namedRepository: familiarizeName(r.namedRepository),
tag: r.tag,
digest: r.digest,
}
}
func (r repository) Familiar() Named {
return familiarizeName(r)
}
func (t taggedReference) Familiar() Named {
return taggedReference{
namedRepository: familiarizeName(t.namedRepository),
tag: t.tag,
}
}
func (c canonicalReference) Familiar() Named {
return canonicalReference{
namedRepository: familiarizeName(c.namedRepository),
digest: c.digest,
}
}
// TagNameOnly adds the default tag "latest" to a reference if it only has
// a repo name.
func TagNameOnly(ref Named) Named {
if IsNameOnly(ref) {
namedTagged, err := WithTag(ref, defaultTag)
if err != nil {
// Default tag must be valid, to create a NamedTagged
// type with non-validated input the WithTag function
// should be used instead
panic(err)
}
return namedTagged
}
return ref
}
// ParseAnyReference parses a reference string as a possible identifier,
// full digest, or familiar name.
func ParseAnyReference(ref string) (Reference, error) {
if ok := anchoredIdentifierRegexp.MatchString(ref); ok {
return digestReference("sha256:" + ref), nil
}
if dgst, err := digest.Parse(ref); err == nil {
return digestReference(dgst), nil
}
return ParseNormalizedNamed(ref)
}
// ParseAnyReferenceWithSet parses a reference string as a possible short
// identifier to be matched in a digest set, a full digest, or familiar name.
func ParseAnyReferenceWithSet(ref string, ds *digestset.Set) (Reference, error) {
if ok := anchoredShortIdentifierRegexp.MatchString(ref); ok {
dgst, err := ds.Lookup(ref)
if err == nil {
return digestReference(dgst), nil
}
} else {
if dgst, err := digest.Parse(ref); err == nil {
return digestReference(dgst), nil
}
}
return ParseNormalizedNamed(ref)
}

View File

@@ -0,0 +1,433 @@
// Package reference provides a general type to represent any way of referencing images within the registry.
// Its main purpose is to abstract tags and digests (content-addressable hash).
//
// Grammar
//
// reference := name [ ":" tag ] [ "@" digest ]
// name := [domain '/'] path-component ['/' path-component]*
// domain := domain-component ['.' domain-component]* [':' port-number]
// domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
// port-number := /[0-9]+/
// path-component := alpha-numeric [separator alpha-numeric]*
// alpha-numeric := /[a-z0-9]+/
// separator := /[_.]|__|[-]*/
//
// tag := /[\w][\w.-]{0,127}/
//
// digest := digest-algorithm ":" digest-hex
// digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]*
// digest-algorithm-separator := /[+.-_]/
// digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/
// digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value
//
// identifier := /[a-f0-9]{64}/
// short-identifier := /[a-f0-9]{6,64}/
package reference
import (
"errors"
"fmt"
"strings"
"github.com/opencontainers/go-digest"
)
const (
// NameTotalLengthMax is the maximum total number of characters in a repository name.
NameTotalLengthMax = 255
)
var (
// ErrReferenceInvalidFormat represents an error while trying to parse a string as a reference.
ErrReferenceInvalidFormat = errors.New("invalid reference format")
// ErrTagInvalidFormat represents an error while trying to parse a string as a tag.
ErrTagInvalidFormat = errors.New("invalid tag format")
// ErrDigestInvalidFormat represents an error while trying to parse a string as a tag.
ErrDigestInvalidFormat = errors.New("invalid digest format")
// ErrNameContainsUppercase is returned for invalid repository names that contain uppercase characters.
ErrNameContainsUppercase = errors.New("repository name must be lowercase")
// ErrNameEmpty is returned for empty, invalid repository names.
ErrNameEmpty = errors.New("repository name must have at least one component")
// ErrNameTooLong is returned when a repository name is longer than NameTotalLengthMax.
ErrNameTooLong = fmt.Errorf("repository name must not be more than %v characters", NameTotalLengthMax)
// ErrNameNotCanonical is returned when a name is not canonical.
ErrNameNotCanonical = errors.New("repository name must be canonical")
)
// Reference is an opaque object reference identifier that may include
// modifiers such as a hostname, name, tag, and digest.
type Reference interface {
// String returns the full reference
String() string
}
// Field provides a wrapper type for resolving correct reference types when
// working with encoding.
type Field struct {
reference Reference
}
// AsField wraps a reference in a Field for encoding.
func AsField(reference Reference) Field {
return Field{reference}
}
// Reference unwraps the reference type from the field to
// return the Reference object. This object should be
// of the appropriate type to further check for different
// reference types.
func (f Field) Reference() Reference {
return f.reference
}
// MarshalText serializes the field to byte text which
// is the string of the reference.
func (f Field) MarshalText() (p []byte, err error) {
return []byte(f.reference.String()), nil
}
// UnmarshalText parses text bytes by invoking the
// reference parser to ensure the appropriately
// typed reference object is wrapped by field.
func (f *Field) UnmarshalText(p []byte) error {
r, err := Parse(string(p))
if err != nil {
return err
}
f.reference = r
return nil
}
// Named is an object with a full name
type Named interface {
Reference
Name() string
}
// Tagged is an object which has a tag
type Tagged interface {
Reference
Tag() string
}
// NamedTagged is an object including a name and tag.
type NamedTagged interface {
Named
Tag() string
}
// Digested is an object which has a digest
// in which it can be referenced by
type Digested interface {
Reference
Digest() digest.Digest
}
// Canonical reference is an object with a fully unique
// name including a name with domain and digest
type Canonical interface {
Named
Digest() digest.Digest
}
// namedRepository is a reference to a repository with a name.
// A namedRepository has both domain and path components.
type namedRepository interface {
Named
Domain() string
Path() string
}
// Domain returns the domain part of the Named reference
func Domain(named Named) string {
if r, ok := named.(namedRepository); ok {
return r.Domain()
}
domain, _ := splitDomain(named.Name())
return domain
}
// Path returns the name without the domain part of the Named reference
func Path(named Named) (name string) {
if r, ok := named.(namedRepository); ok {
return r.Path()
}
_, path := splitDomain(named.Name())
return path
}
func splitDomain(name string) (string, string) {
match := anchoredNameRegexp.FindStringSubmatch(name)
if len(match) != 3 {
return "", name
}
return match[1], match[2]
}
// SplitHostname splits a named reference into a
// hostname and name string. If no valid hostname is
// found, the hostname is empty and the full value
// is returned as name
// DEPRECATED: Use Domain or Path
func SplitHostname(named Named) (string, string) {
if r, ok := named.(namedRepository); ok {
return r.Domain(), r.Path()
}
return splitDomain(named.Name())
}
// Parse parses s and returns a syntactically valid Reference.
// If an error was encountered it is returned, along with a nil Reference.
// NOTE: Parse will not handle short digests.
func Parse(s string) (Reference, error) {
matches := ReferenceRegexp.FindStringSubmatch(s)
if matches == nil {
if s == "" {
return nil, ErrNameEmpty
}
if ReferenceRegexp.FindStringSubmatch(strings.ToLower(s)) != nil {
return nil, ErrNameContainsUppercase
}
return nil, ErrReferenceInvalidFormat
}
if len(matches[1]) > NameTotalLengthMax {
return nil, ErrNameTooLong
}
var repo repository
nameMatch := anchoredNameRegexp.FindStringSubmatch(matches[1])
if len(nameMatch) == 3 {
repo.domain = nameMatch[1]
repo.path = nameMatch[2]
} else {
repo.domain = ""
repo.path = matches[1]
}
ref := reference{
namedRepository: repo,
tag: matches[2],
}
if matches[3] != "" {
var err error
ref.digest, err = digest.Parse(matches[3])
if err != nil {
return nil, err
}
}
r := getBestReferenceType(ref)
if r == nil {
return nil, ErrNameEmpty
}
return r, nil
}
// ParseNamed parses s and returns a syntactically valid reference implementing
// the Named interface. The reference must have a name and be in the canonical
// form, otherwise an error is returned.
// If an error was encountered it is returned, along with a nil Reference.
// NOTE: ParseNamed will not handle short digests.
func ParseNamed(s string) (Named, error) {
named, err := ParseNormalizedNamed(s)
if err != nil {
return nil, err
}
if named.String() != s {
return nil, ErrNameNotCanonical
}
return named, nil
}
// WithName returns a named object representing the given string. If the input
// is invalid ErrReferenceInvalidFormat will be returned.
func WithName(name string) (Named, error) {
if len(name) > NameTotalLengthMax {
return nil, ErrNameTooLong
}
match := anchoredNameRegexp.FindStringSubmatch(name)
if match == nil || len(match) != 3 {
return nil, ErrReferenceInvalidFormat
}
return repository{
domain: match[1],
path: match[2],
}, nil
}
// WithTag combines the name from "name" and the tag from "tag" to form a
// reference incorporating both the name and the tag.
func WithTag(name Named, tag string) (NamedTagged, error) {
if !anchoredTagRegexp.MatchString(tag) {
return nil, ErrTagInvalidFormat
}
var repo repository
if r, ok := name.(namedRepository); ok {
repo.domain = r.Domain()
repo.path = r.Path()
} else {
repo.path = name.Name()
}
if canonical, ok := name.(Canonical); ok {
return reference{
namedRepository: repo,
tag: tag,
digest: canonical.Digest(),
}, nil
}
return taggedReference{
namedRepository: repo,
tag: tag,
}, nil
}
// WithDigest combines the name from "name" and the digest from "digest" to form
// a reference incorporating both the name and the digest.
func WithDigest(name Named, digest digest.Digest) (Canonical, error) {
if !anchoredDigestRegexp.MatchString(digest.String()) {
return nil, ErrDigestInvalidFormat
}
var repo repository
if r, ok := name.(namedRepository); ok {
repo.domain = r.Domain()
repo.path = r.Path()
} else {
repo.path = name.Name()
}
if tagged, ok := name.(Tagged); ok {
return reference{
namedRepository: repo,
tag: tagged.Tag(),
digest: digest,
}, nil
}
return canonicalReference{
namedRepository: repo,
digest: digest,
}, nil
}
// TrimNamed removes any tag or digest from the named reference.
func TrimNamed(ref Named) Named {
domain, path := SplitHostname(ref)
return repository{
domain: domain,
path: path,
}
}
func getBestReferenceType(ref reference) Reference {
if ref.Name() == "" {
// Allow digest only references
if ref.digest != "" {
return digestReference(ref.digest)
}
return nil
}
if ref.tag == "" {
if ref.digest != "" {
return canonicalReference{
namedRepository: ref.namedRepository,
digest: ref.digest,
}
}
return ref.namedRepository
}
if ref.digest == "" {
return taggedReference{
namedRepository: ref.namedRepository,
tag: ref.tag,
}
}
return ref
}
type reference struct {
namedRepository
tag string
digest digest.Digest
}
func (r reference) String() string {
return r.Name() + ":" + r.tag + "@" + r.digest.String()
}
func (r reference) Tag() string {
return r.tag
}
func (r reference) Digest() digest.Digest {
return r.digest
}
type repository struct {
domain string
path string
}
func (r repository) String() string {
return r.Name()
}
func (r repository) Name() string {
if r.domain == "" {
return r.path
}
return r.domain + "/" + r.path
}
func (r repository) Domain() string {
return r.domain
}
func (r repository) Path() string {
return r.path
}
type digestReference digest.Digest
func (d digestReference) String() string {
return digest.Digest(d).String()
}
func (d digestReference) Digest() digest.Digest {
return digest.Digest(d)
}
type taggedReference struct {
namedRepository
tag string
}
func (t taggedReference) String() string {
return t.Name() + ":" + t.tag
}
func (t taggedReference) Tag() string {
return t.tag
}
type canonicalReference struct {
namedRepository
digest digest.Digest
}
func (c canonicalReference) String() string {
return c.Name() + "@" + c.digest.String()
}
func (c canonicalReference) Digest() digest.Digest {
return c.digest
}

View File

@@ -0,0 +1,147 @@
package reference
import "regexp"
var (
// alphaNumericRegexp defines the alpha numeric atom, typically a
// component of names. This only allows lower case characters and digits.
alphaNumericRegexp = match(`[a-z0-9]+`)
// separatorRegexp defines the separators allowed to be embedded in name
// components. This allow one period, one or two underscore and multiple
// dashes. Repeated dashes and underscores are intentionally treated
// differently. In order to support valid hostnames as name components,
// supporting repeated dash was added. Additionally double underscore is
// now allowed as a separator to loosen the restriction for previously
// supported names.
separatorRegexp = match(`(?:[._]|__|[-]*)`)
// nameComponentRegexp restricts registry path component names to start
// with at least one letter or number, with following parts able to be
// separated by one period, one or two underscore and multiple dashes.
nameComponentRegexp = expression(
alphaNumericRegexp,
optional(repeated(separatorRegexp, alphaNumericRegexp)))
// domainComponentRegexp restricts the registry domain component of a
// repository name to start with a component as defined by DomainRegexp
// and followed by an optional port.
domainComponentRegexp = match(`(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`)
// DomainRegexp defines the structure of potential domain components
// that may be part of image names. This is purposely a subset of what is
// allowed by DNS to ensure backwards compatibility with Docker image
// names.
DomainRegexp = expression(
domainComponentRegexp,
optional(repeated(literal(`.`), domainComponentRegexp)),
optional(literal(`:`), match(`[0-9]+`)))
// TagRegexp matches valid tag names. From docker/docker:graph/tags.go.
TagRegexp = match(`[\w][\w.-]{0,127}`)
// anchoredTagRegexp matches valid tag names, anchored at the start and
// end of the matched string.
anchoredTagRegexp = anchored(TagRegexp)
// DigestRegexp matches valid digests.
DigestRegexp = match(`[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`)
// anchoredDigestRegexp matches valid digests, anchored at the start and
// end of the matched string.
anchoredDigestRegexp = anchored(DigestRegexp)
// NameRegexp is the format for the name component of references. The
// regexp has capturing groups for the domain and name part omitting
// the separating forward slash from either.
NameRegexp = expression(
optional(DomainRegexp, literal(`/`)),
nameComponentRegexp,
optional(repeated(literal(`/`), nameComponentRegexp)))
// anchoredNameRegexp is used to parse a name value, capturing the
// domain and trailing components.
anchoredNameRegexp = anchored(
optional(capture(DomainRegexp), literal(`/`)),
capture(nameComponentRegexp,
optional(repeated(literal(`/`), nameComponentRegexp))))
// ReferenceRegexp is the full supported format of a reference. The regexp
// is anchored and has capturing groups for name, tag, and digest
// components.
ReferenceRegexp = anchored(capture(NameRegexp),
optional(literal(":"), capture(TagRegexp)),
optional(literal("@"), capture(DigestRegexp)))
// IdentifierRegexp is the format for string identifier used as a
// content addressable identifier using sha256. These identifiers
// are like digests without the algorithm, since sha256 is used.
IdentifierRegexp = match(`([a-f0-9]{64})`)
// ShortIdentifierRegexp is the format used to represent a prefix
// of an identifier. A prefix may be used to match a sha256 identifier
// within a list of trusted identifiers.
ShortIdentifierRegexp = match(`([a-f0-9]{6,64})`)
// anchoredIdentifierRegexp is used to check or match an
// identifier value, anchored at start and end of string.
anchoredIdentifierRegexp = anchored(IdentifierRegexp)
// anchoredShortIdentifierRegexp is used to check if a value
// is a possible identifier prefix, anchored at start and end
// of string.
anchoredShortIdentifierRegexp = anchored(ShortIdentifierRegexp)
)
// match compiles the string to a regular expression.
var match = regexp.MustCompile
// literal compiles s into a literal regular expression, escaping any regexp
// reserved characters.
func literal(s string) *regexp.Regexp {
re := match(regexp.QuoteMeta(s))
if _, complete := re.LiteralPrefix(); !complete {
panic("must be a literal")
}
return re
}
// expression defines a full expression, where each regular expression must
// follow the previous.
func expression(res ...*regexp.Regexp) *regexp.Regexp {
var s string
for _, re := range res {
s += re.String()
}
return match(s)
}
// optional wraps the expression in a non-capturing group and makes the
// production optional.
func optional(res ...*regexp.Regexp) *regexp.Regexp {
return match(group(expression(res...)).String() + `?`)
}
// repeated wraps the regexp in a non-capturing group to get one or more
// matches.
func repeated(res ...*regexp.Regexp) *regexp.Regexp {
return match(group(expression(res...)).String() + `+`)
}
// group wraps the regexp in a non-capturing group.
func group(res ...*regexp.Regexp) *regexp.Regexp {
return match(`(?:` + expression(res...).String() + `)`)
}
// capture wraps the expression in a capturing group.
func capture(res ...*regexp.Regexp) *regexp.Regexp {
return match(`(` + expression(res...).String() + `)`)
}
// anchored anchors the regular expression by adding start and end delimiters.
func anchored(res ...*regexp.Regexp) *regexp.Regexp {
return match(`^` + expression(res...).String() + `$`)
}

9
vendor/modules.txt vendored
View File

@@ -137,7 +137,7 @@ github.com/danieljoos/wincred
# github.com/davecgh/go-spew v1.1.1
## explicit
github.com/davecgh/go-spew/spew
# github.com/devfile/api/v2 v2.2.0
# github.com/devfile/api/v2 v2.2.1-alpha.0.20230413012049-a6c32fca0dbd
## explicit; go 1.18
github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2
github.com/devfile/api/v2/pkg/attributes
@@ -146,7 +146,7 @@ github.com/devfile/api/v2/pkg/utils/overriding
github.com/devfile/api/v2/pkg/utils/unions
github.com/devfile/api/v2/pkg/validation
github.com/devfile/api/v2/pkg/validation/variables
# github.com/devfile/library/v2 v2.2.1-0.20230330160000-c1b23d25e652
# github.com/devfile/library/v2 v2.2.1-0.20230515084048-f041d798707c
## explicit; go 1.18
github.com/devfile/library/v2/pkg/devfile
github.com/devfile/library/v2/pkg/devfile/generator
@@ -159,6 +159,7 @@ github.com/devfile/library/v2/pkg/devfile/parser/data/v2/2.1.0
github.com/devfile/library/v2/pkg/devfile/parser/data/v2/2.2.0
github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common
github.com/devfile/library/v2/pkg/devfile/validate
github.com/devfile/library/v2/pkg/git
github.com/devfile/library/v2/pkg/testingutil
github.com/devfile/library/v2/pkg/testingutil/filesystem
github.com/devfile/library/v2/pkg/util
@@ -168,6 +169,10 @@ github.com/devfile/registry-support/index/generator/schema
# github.com/devfile/registry-support/registry-library v0.0.0-20221201200738-19293ac0b8ab
## explicit; go 1.14
github.com/devfile/registry-support/registry-library/library
# github.com/distribution/distribution/v3 v3.0.0-20211118083504-a29a3c99a684
## explicit; go 1.16
github.com/distribution/distribution/v3/digestset
github.com/distribution/distribution/v3/reference
# github.com/docker/cli v20.10.13+incompatible
## explicit
github.com/docker/cli/cli/config