mirror of https://github.com/kubevela/kubevela.git
Feat: support url in ref-objects (#4240)
Signed-off-by: Somefive <yd219913@alibaba-inc.com>
This commit is contained in:
parent
c4e1f39d28
commit
92fa67cd69
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue