diff --git a/apis/core.oam.dev/v1alpha1/component_types.go b/apis/core.oam.dev/v1alpha1/component_types.go index 30cca03fb..62bd546c7 100644 --- a/apis/core.oam.dev/v1alpha1/component_types.go +++ b/apis/core.oam.dev/v1alpha1/component_types.go @@ -25,6 +25,8 @@ const ( type RefObjectsComponentSpec struct { // Objects the referrers to the Kubernetes objects Objects []ObjectReferrer `json:"objects,omitempty"` + // URLs are the links that stores the referred objects + URLs []string `json:"urls,omitempty"` } // ObjectReferrer selects Kubernetes objects diff --git a/apis/core.oam.dev/v1alpha1/zz_generated.deepcopy.go b/apis/core.oam.dev/v1alpha1/zz_generated.deepcopy.go index f63c6bf46..43c6e9065 100644 --- a/apis/core.oam.dev/v1alpha1/zz_generated.deepcopy.go +++ b/apis/core.oam.dev/v1alpha1/zz_generated.deepcopy.go @@ -595,6 +595,11 @@ func (in *RefObjectsComponentSpec) DeepCopyInto(out *RefObjectsComponentSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.URLs != nil { + in, out := &in.URLs, &out.URLs + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RefObjectsComponentSpec. diff --git a/charts/vela-core/templates/defwithtemplate/ref-objects.yaml b/charts/vela-core/templates/defwithtemplate/ref-objects.yaml index 5c1701ae5..8176cb11a 100644 --- a/charts/vela-core/templates/defwithtemplate/ref-objects.yaml +++ b/charts/vela-core/templates/defwithtemplate/ref-objects.yaml @@ -22,7 +22,12 @@ spec: } ... } - output: parameter.objects[0] + output: { + if len(parameter.objects) > 0 { + parameter.objects[0] + } + ... + } outputs: { for i, v in parameter.objects { if i > 0 { diff --git a/charts/vela-minimal/templates/defwithtemplate/ref-objects.yaml b/charts/vela-minimal/templates/defwithtemplate/ref-objects.yaml index 5c1701ae5..8176cb11a 100644 --- a/charts/vela-minimal/templates/defwithtemplate/ref-objects.yaml +++ b/charts/vela-minimal/templates/defwithtemplate/ref-objects.yaml @@ -22,7 +22,12 @@ spec: } ... } - output: parameter.objects[0] + output: { + if len(parameter.objects) > 0 { + parameter.objects[0] + } + ... + } outputs: { for i, v in parameter.objects { if i > 0 { diff --git a/pkg/appfile/appfile.go b/pkg/appfile/appfile.go index 13637fbab..bf47cff71 100644 --- a/pkg/appfile/appfile.go +++ b/pkg/appfile/appfile.go @@ -51,6 +51,7 @@ import ( "github.com/oam-dev/kubevela/pkg/cue/process" "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/oam/util" + utilscommon "github.com/oam-dev/kubevela/pkg/utils/common" ) // constant error information @@ -931,6 +932,13 @@ func (af *Appfile) LoadDynamicComponent(ctx context.Context, cli client.Client, } uns = component.AppendUnstructuredObjects(uns, objs...) } + // nolint + for _, url := range spec.URLs { + objs := utilscommon.FilterObjectsByCondition(af.ReferredObjects, func(obj *unstructured.Unstructured) bool { + return obj.GetAnnotations() != nil && obj.GetAnnotations()[oam.AnnotationResourceURL] == url + }) + uns = component.AppendUnstructuredObjects(uns, objs...) + } refObjs, err := component.ConvertUnstructuredsToReferredObjects(uns) if err != nil { return nil, errors.Wrapf(err, "failed to marshal referred object") diff --git a/pkg/appfile/parser.go b/pkg/appfile/parser.go index 4b622916f..d7c3dd78e 100644 --- a/pkg/appfile/parser.go +++ b/pkg/appfile/parser.go @@ -45,6 +45,7 @@ import ( "github.com/oam-dev/kubevela/pkg/oam/util" policypkg "github.com/oam-dev/kubevela/pkg/policy" "github.com/oam-dev/kubevela/pkg/utils" + utilscommon "github.com/oam-dev/kubevela/pkg/utils/common" "github.com/oam-dev/kubevela/pkg/workflow/step" wftypes "github.com/oam-dev/kubevela/pkg/workflow/types" ) @@ -349,6 +350,16 @@ func (p *Parser) parseReferredObjects(ctx context.Context, af *Appfile) error { } af.ReferredObjects = component.AppendUnstructuredObjects(af.ReferredObjects, objs...) } + for _, url := range spec.URLs { + objs, err := utilscommon.HTTPGetKubernetesObjects(ctx, url) + if err != nil { + return fmt.Errorf("failed to load Kubernetes objects from url %s: %w", url, err) + } + for _, obj := range objs { + util.AddAnnotations(obj, map[string]string{oam.AnnotationResourceURL: url}) + } + af.ReferredObjects = component.AppendUnstructuredObjects(af.ReferredObjects, objs...) + } } sort.Slice(af.ReferredObjects, func(i, j int) bool { a, b := af.ReferredObjects[i], af.ReferredObjects[j] diff --git a/pkg/component/ref_objects.go b/pkg/component/ref_objects.go index 362970efd..b32cbadf5 100644 --- a/pkg/component/ref_objects.go +++ b/pkg/component/ref_objects.go @@ -37,6 +37,8 @@ import ( velaclient "github.com/oam-dev/kubevela/pkg/client" "github.com/oam-dev/kubevela/pkg/features" "github.com/oam-dev/kubevela/pkg/multicluster" + "github.com/oam-dev/kubevela/pkg/oam" + utilscommon "github.com/oam-dev/kubevela/pkg/utils/common" ) const ( @@ -168,6 +170,9 @@ func SelectRefObjectsForDispatch(ctx context.Context, cli client.Client, appNs s // ReferredObjectsDelegatingClient delegate client get/list function by retrieving ref-objects from existing objects func ReferredObjectsDelegatingClient(cli client.Client, objs []*unstructured.Unstructured) client.Client { + objs = utilscommon.FilterObjectsByCondition(objs, func(obj *unstructured.Unstructured) bool { + return obj.GetAnnotations() == nil || obj.GetAnnotations()[oam.AnnotationResourceURL] == "" + }) return velaclient.DelegatingHandlerClient{ Client: cli, Getter: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { diff --git a/pkg/oam/labels.go b/pkg/oam/labels.go index 06a212ccb..f3d296748 100644 --- a/pkg/oam/labels.go +++ b/pkg/oam/labels.go @@ -216,4 +216,7 @@ const ( // AnnotationAppSharedBy records who share the application AnnotationAppSharedBy = "app.oam.dev/shared-by" + + // AnnotationResourceURL records the source url of the Kubernetes object + AnnotationResourceURL = "app.oam.dev/resource-url" ) diff --git a/pkg/utils/common/common.go b/pkg/utils/common/common.go index 6fa3f31a9..a2da0114d 100644 --- a/pkg/utils/common/common.go +++ b/pkg/utils/common/common.go @@ -40,6 +40,7 @@ import ( kruisev1alpha1 "github.com/openkruise/rollouts/api/v1alpha1" errors2 "github.com/pkg/errors" certmanager "github.com/wonderflow/cert-manager-api/pkg/apis/certmanager/v1" + yamlv3 "gopkg.in/yaml.v3" istioclientv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" v1 "k8s.io/api/core/v1" crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -146,8 +147,8 @@ func GetClient() (client.Client, error) { return nil, errors.New("client not set, call SetGlobalClient first") } -// HTTPGetWithOption use HTTP option and default client to send get request -func HTTPGetWithOption(ctx context.Context, url string, opts *HTTPOption) ([]byte, error) { +// HTTPGetResponse use HTTP option and default client to send request and get raw response +func HTTPGetResponse(ctx context.Context, url string, opts *HTTPOption) (*http.Response, error) { // Change NewRequest to NewRequestWithContext and pass context it req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { @@ -156,7 +157,12 @@ func HTTPGetWithOption(ctx context.Context, url string, opts *HTTPOption) ([]byt if opts != nil && len(opts.Username) != 0 && len(opts.Password) != 0 { req.SetBasicAuth(opts.Username, opts.Password) } - resp, err := http.DefaultClient.Do(req) + return http.DefaultClient.Do(req) +} + +// HTTPGetWithOption use HTTP option and default client to send get request +func HTTPGetWithOption(ctx context.Context, url string, opts *HTTPOption) ([]byte, error) { + resp, err := HTTPGetResponse(ctx, url, opts) if err != nil { return nil, err } @@ -165,6 +171,29 @@ func HTTPGetWithOption(ctx context.Context, url string, opts *HTTPOption) ([]byt return io.ReadAll(resp.Body) } +// HTTPGetKubernetesObjects use HTTP requests to load resources from remote url +func HTTPGetKubernetesObjects(ctx context.Context, url string) ([]*unstructured.Unstructured, error) { + resp, err := HTTPGetResponse(ctx, url, nil) + if err != nil { + return nil, err + } + //nolint:errcheck + defer resp.Body.Close() + decoder := yamlv3.NewDecoder(resp.Body) + var uns []*unstructured.Unstructured + for { + obj := &unstructured.Unstructured{Object: map[string]interface{}{}} + if err := decoder.Decode(obj.Object); err != nil { + if errors.Is(err, io.EOF) { + break + } + return nil, fmt.Errorf("failed to decode object: %w", err) + } + uns = append(uns, obj) + } + return uns, nil +} + // GetCUEParameterValue converts definitions to cue format func GetCUEParameterValue(cueStr string, pd *packages.PackageDiscover) (cue.Value, error) { var template *cue.Instance @@ -566,3 +595,13 @@ func NewK8sClient() (client.Client, error) { } return k8sClient, nil } + +// FilterObjectsByCondition filter object slices by condition function +func FilterObjectsByCondition(objs []*unstructured.Unstructured, filter func(unstructured2 *unstructured.Unstructured) bool) (outs []*unstructured.Unstructured) { + for _, obj := range objs { + if filter(obj) { + outs = append(outs, obj) + } + } + return +} diff --git a/pkg/utils/common/common_test.go b/pkg/utils/common/common_test.go index ea44fc8d8..faeb78ffc 100644 --- a/pkg/utils/common/common_test.go +++ b/pkg/utils/common/common_test.go @@ -508,3 +508,15 @@ func TestRemoveEmptyString(t *testing.T) { assert.NotEmpty(t, s) } } + +func TestHTTPGetKubernetesObjects(t *testing.T) { + _, err := HTTPGetKubernetesObjects(context.Background(), "invalid-url") + assert.NotNil(t, err) + uns, err := HTTPGetKubernetesObjects(context.Background(), "https://gist.githubusercontent.com/Somefive/b189219a9222eaa70b8908cf4379402b/raw/920e83b1a2d56b584f9d8c7a97810a505a0bbaad/example-busybox-resources.yaml") + assert.NoError(t, err) + assert.Equal(t, 2, len(uns)) + assert.Equal(t, "busybox", uns[0].GetName()) + assert.Equal(t, "Deployment", uns[0].GetKind()) + assert.Equal(t, "busybox", uns[1].GetName()) + assert.Equal(t, "ConfigMap", uns[1].GetKind()) +} diff --git a/pkg/workflow/step/generator.go b/pkg/workflow/step/generator.go index 3b1e2de6f..1018d6845 100644 --- a/pkg/workflow/step/generator.go +++ b/pkg/workflow/step/generator.go @@ -156,7 +156,23 @@ func (g *DeployWorkflowStepGenerator) Generate(app *v1beta1.Application, existin }), }) } - return + if len(topologies) == 0 { + containsRefObjects := false + for _, comp := range app.Spec.Components { + if comp.Type == v1alpha1.RefObjectsComponentType { + containsRefObjects = true + break + } + } + if containsRefObjects { + steps = append(steps, v1beta1.WorkflowStep{ + Name: "deploy", + Type: "deploy", + Properties: util.Object2RawExtension(map[string]interface{}{"policies": append([]string{}, overrides...)}), + }) + } + } + return steps, nil } // DeployPreApproveWorkflowStepGenerator generate suspend workflow steps before all deploy steps diff --git a/pkg/workflow/step/generator_test.go b/pkg/workflow/step/generator_test.go index 87f1a4aa0..91c5adb9c 100644 --- a/pkg/workflow/step/generator_test.go +++ b/pkg/workflow/step/generator_test.go @@ -166,6 +166,22 @@ func TestWorkflowStepGenerator(t *testing.T) { Properties: &runtime.RawExtension{Raw: []byte(`{"policies":["example-override-policy-1","example-override-policy-2","example-topology-policy-2"]}`)}, }}, }, + "deploy-with-ref-without-po-workflow": { + input: []v1beta1.WorkflowStep{}, + app: &v1beta1.Application{ + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{{ + Name: "example-comp", + Type: "ref-objects", + }}, + }, + }, + output: []v1beta1.WorkflowStep{{ + Name: "deploy", + Type: "deploy", + Properties: &runtime.RawExtension{Raw: []byte(`{"policies":[]}`)}, + }}, + }, "pre-approve-workflow": { input: []v1beta1.WorkflowStep{{ Name: "deploy-example-topology-policy-1", diff --git a/test/e2e-multicluster-test/multicluster_standalone_test.go b/test/e2e-multicluster-test/multicluster_standalone_test.go index 293c3057e..2f9c730a2 100644 --- a/test/e2e-multicluster-test/multicluster_standalone_test.go +++ b/test/e2e-multicluster-test/multicluster_standalone_test.go @@ -322,4 +322,35 @@ var _ = Describe("Test multicluster standalone scenario", func() { g.Expect(errors.IsNotFound(err)).Should(BeTrue()) }, time.Minute).Should(Succeed()) }) + + It("Test ref-objects with url", func() { + newApp := &v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "app"}, + Spec: v1beta1.ApplicationSpec{ + Components: []oamcomm.ApplicationComponent{{ + Name: "example", + Type: "ref-objects", + Properties: &runtime.RawExtension{Raw: []byte(`{"urls":["https://gist.githubusercontent.com/Somefive/b189219a9222eaa70b8908cf4379402b/raw/920e83b1a2d56b584f9d8c7a97810a505a0bbaad/example-busybox-resources.yaml"]}`)}, + }}, + }, + } + + By("Create application") + Expect(k8sClient.Create(hubCtx, newApp)).Should(Succeed()) + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(hubCtx, client.ObjectKeyFromObject(newApp), newApp)).Should(Succeed()) + g.Expect(newApp.Status.Phase).Should(Equal(oamcomm.ApplicationRunning)) + }, 15*time.Second).Should(Succeed()) + key := types.NamespacedName{Namespace: namespace, Name: "busybox"} + Expect(k8sClient.Get(hubCtx, key, &v1.Deployment{})).Should(Succeed()) + Expect(k8sClient.Get(hubCtx, key, &corev1.ConfigMap{})).Should(Succeed()) + + By("Delete application") + Expect(k8sClient.Delete(hubCtx, newApp)).Should(Succeed()) + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(hubCtx, client.ObjectKeyFromObject(newApp), newApp)).Should(Satisfy(errors.IsNotFound)) + }, 15*time.Second).Should(Succeed()) + Expect(k8sClient.Get(hubCtx, key, &v1.Deployment{})).Should(Satisfy(errors.IsNotFound)) + Expect(k8sClient.Get(hubCtx, key, &corev1.ConfigMap{})).Should(Satisfy(errors.IsNotFound)) + }) }) diff --git a/vela-templates/definitions/internal/component/ref-objects.cue b/vela-templates/definitions/internal/component/ref-objects.cue index 182453203..437549cec 100644 --- a/vela-templates/definitions/internal/component/ref-objects.cue +++ b/vela-templates/definitions/internal/component/ref-objects.cue @@ -64,7 +64,12 @@ template: { ... } - output: parameter.objects[0] + output: { + if len(parameter.objects) > 0 { + parameter.objects[0] + } + ... + } outputs: { for i, v in parameter.objects {