feat: Enhance JSBundle Api to declare auxiliary asset files required for extending components (#6308)
feat: add support for auxiliary asset files to jsBundle Signed-off-by: lingbo <lingbo@lingbohome.com>
This commit is contained in:
@@ -3,7 +3,8 @@ apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: (unknown)
|
||||
controller-gen.kubebuilder.io/version: (devel)
|
||||
creationTimestamp: null
|
||||
name: jsbundles.extensions.kubesphere.io
|
||||
spec:
|
||||
group: extensions.kubesphere.io
|
||||
@@ -17,29 +18,119 @@ spec:
|
||||
- name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: |-
|
||||
JSBundle declares a js bundle that needs to be injected into ks-console,
|
||||
the endpoint can be provided by a service or a static file.
|
||||
description: JSBundle declares a js bundle that needs to be injected into
|
||||
ks-console, the endpoint can be provided by a service or a static file.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
properties:
|
||||
assets:
|
||||
properties:
|
||||
files:
|
||||
items:
|
||||
properties:
|
||||
caBundle:
|
||||
format: byte
|
||||
type: string
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
link:
|
||||
type: string
|
||||
mimeType:
|
||||
description: Set the MIME Type of the file, if not specified,
|
||||
it will be provided by the content-type response header
|
||||
in the upstream service by default.
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
service:
|
||||
description: service is a reference to the service for this
|
||||
endpoint. Either service or url must be specified. the
|
||||
scheme is default to HTTPS.
|
||||
properties:
|
||||
name:
|
||||
description: name is the name of the service. Required
|
||||
type: string
|
||||
namespace:
|
||||
description: namespace is the namespace of the service.
|
||||
Required
|
||||
type: string
|
||||
path:
|
||||
description: path is an optional URL path at which the
|
||||
upstream will be contacted.
|
||||
type: string
|
||||
port:
|
||||
description: port is an optional service port at which
|
||||
the upstream will be contacted. `port` should be a
|
||||
valid port number (1-65535, inclusive). Defaults to
|
||||
443 for backward compatibility.
|
||||
format: int32
|
||||
type: integer
|
||||
required:
|
||||
- name
|
||||
- namespace
|
||||
type: object
|
||||
url:
|
||||
description: '`url` gives the location of the upstream,
|
||||
in standard URL form (`scheme://host:port/path`). Exactly
|
||||
one of `url` or `service` must be specified.'
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
style:
|
||||
properties:
|
||||
caBundle:
|
||||
format: byte
|
||||
type: string
|
||||
insecureSkipVerify:
|
||||
type: boolean
|
||||
link:
|
||||
type: string
|
||||
service:
|
||||
description: service is a reference to the service for this
|
||||
endpoint. Either service or url must be specified. the scheme
|
||||
is default to HTTPS.
|
||||
properties:
|
||||
name:
|
||||
description: name is the name of the service. Required
|
||||
type: string
|
||||
namespace:
|
||||
description: namespace is the namespace of the service.
|
||||
Required
|
||||
type: string
|
||||
path:
|
||||
description: path is an optional URL path at which the
|
||||
upstream will be contacted.
|
||||
type: string
|
||||
port:
|
||||
description: port is an optional service port at which
|
||||
the upstream will be contacted. `port` should be a valid
|
||||
port number (1-65535, inclusive). Defaults to 443 for
|
||||
backward compatibility.
|
||||
format: int32
|
||||
type: integer
|
||||
required:
|
||||
- name
|
||||
- namespace
|
||||
type: object
|
||||
url:
|
||||
description: '`url` gives the location of the upstream, in
|
||||
standard URL form (`scheme://host:port/path`). Exactly one
|
||||
of `url` or `service` must be specified.'
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
raw:
|
||||
format: byte
|
||||
type: string
|
||||
@@ -55,13 +146,8 @@ spec:
|
||||
description: The key to select.
|
||||
type: string
|
||||
name:
|
||||
default: ""
|
||||
description: |-
|
||||
Name of the referent.
|
||||
This field is effectively required, but due to backwards compatibility is
|
||||
allowed to be empty. Instances of this type with an empty value here are
|
||||
almost certainly wrong.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
TODO: Add other useful fields. apiVersion, kind, uid?'
|
||||
type: string
|
||||
namespace:
|
||||
type: string
|
||||
@@ -84,13 +170,8 @@ spec:
|
||||
a valid secret key.
|
||||
type: string
|
||||
name:
|
||||
default: ""
|
||||
description: |-
|
||||
Name of the referent.
|
||||
This field is effectively required, but due to backwards compatibility is
|
||||
allowed to be empty. Instances of this type with an empty value here are
|
||||
almost certainly wrong.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
TODO: Add other useful fields. apiVersion, kind, uid?'
|
||||
type: string
|
||||
namespace:
|
||||
type: string
|
||||
@@ -104,30 +185,25 @@ spec:
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
service:
|
||||
description: |-
|
||||
service is a reference to the service for this endpoint. Either
|
||||
service or url must be specified.
|
||||
the scheme is default to HTTPS.
|
||||
description: service is a reference to the service for this endpoint.
|
||||
Either service or url must be specified. the scheme is default
|
||||
to HTTPS.
|
||||
properties:
|
||||
name:
|
||||
description: |-
|
||||
name is the name of the service.
|
||||
Required
|
||||
description: name is the name of the service. Required
|
||||
type: string
|
||||
namespace:
|
||||
description: |-
|
||||
namespace is the namespace of the service.
|
||||
Required
|
||||
description: namespace is the namespace of the service. Required
|
||||
type: string
|
||||
path:
|
||||
description: path is an optional URL path at which the upstream
|
||||
will be contacted.
|
||||
type: string
|
||||
port:
|
||||
description: |-
|
||||
port is an optional service port at which the upstream will be contacted.
|
||||
`port` should be a valid port number (1-65535, inclusive).
|
||||
Defaults to 443 for backward compatibility.
|
||||
description: port is an optional service port at which the
|
||||
upstream will be contacted. `port` should be a valid port
|
||||
number (1-65535, inclusive). Defaults to 443 for backward
|
||||
compatibility.
|
||||
format: int32
|
||||
type: integer
|
||||
required:
|
||||
@@ -135,10 +211,9 @@ spec:
|
||||
- namespace
|
||||
type: object
|
||||
url:
|
||||
description: |-
|
||||
`url` gives the location of the upstream, in standard URL form
|
||||
(`scheme://host:port/path`). Exactly one of `url` or `service`
|
||||
must be specified.
|
||||
description: '`url` gives the location of the upstream, in standard
|
||||
URL form (`scheme://host:port/path`). Exactly one of `url` or
|
||||
`service` must be specified.'
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
@@ -146,35 +221,43 @@ spec:
|
||||
properties:
|
||||
conditions:
|
||||
items:
|
||||
description: Condition contains details for one aspect of the current
|
||||
state of this API Resource.
|
||||
description: "Condition contains details for one aspect of the current
|
||||
state of this API Resource. --- This struct is intended for direct
|
||||
use as an array at the field path .status.conditions. For example,
|
||||
\n type FooStatus struct{ // Represents the observations of a
|
||||
foo's current state. // Known .status.conditions.type are: \"Available\",
|
||||
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
|
||||
// +listType=map // +listMapKey=type Conditions []metav1.Condition
|
||||
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
|
||||
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: |-
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
description: lastTransitionTime is the last time the condition
|
||||
transitioned from one status to another. This should be when
|
||||
the underlying condition changed. If that is not known, then
|
||||
using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: |-
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.
|
||||
description: message is a human readable message indicating
|
||||
details about the transition. This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: |-
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.
|
||||
description: observedGeneration represents the .metadata.generation
|
||||
that the condition was set based upon. For instance, if .metadata.generation
|
||||
is currently 12, but the .status.conditions[x].observedGeneration
|
||||
is 9, the condition is out of date with respect to the current
|
||||
state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: |-
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
description: reason contains a programmatic identifier indicating
|
||||
the reason for the condition's last transition. Producers
|
||||
of specific condition types may define expected values and
|
||||
meanings for this field, and whether the values are considered
|
||||
a guaranteed API. The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
@@ -189,6 +272,10 @@ spec:
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
--- Many .condition.type values are consistent across resources
|
||||
like Available, but because arbitrary conditions can be useful
|
||||
(see .node.status.conditions), the ability to deconflict is
|
||||
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
|
||||
@@ -74,6 +74,24 @@ func (s *jsBundle) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if jsBundle.Status.State == extensionsv1alpha1.StateAvailable && jsBundle.Spec.Assets.Style != nil &&
|
||||
jsBundle.Spec.Assets.Style.Link == requestInfo.Path {
|
||||
s.rawFromRemote(jsBundle.Spec.Assets.Style.Endpoint, w, req)
|
||||
return
|
||||
}
|
||||
|
||||
if jsBundle.Status.State == extensionsv1alpha1.StateAvailable && jsBundle.Spec.Assets.Files != nil {
|
||||
for _, file := range jsBundle.Spec.Assets.Files {
|
||||
if file.Link == requestInfo.Path {
|
||||
if file.MIMEType != nil && *file.MIMEType != "" {
|
||||
w.Header().Set("Content-Type", *file.MIMEType)
|
||||
}
|
||||
s.rawFromRemote(file.Endpoint, w, req)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s.next.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
@@ -77,7 +77,19 @@ func (r *JSBundleWebhook) validateJSBundle(ctx context.Context, jsBundle *extens
|
||||
}
|
||||
extensionName := jsBundle.Labels[v1alpha1.ExtensionReferenceLabel]
|
||||
if extensionName != "" && !strings.HasPrefix(jsBundle.Status.Link, fmt.Sprintf("/dist/%s", extensionName)) {
|
||||
return nil, fmt.Errorf("the prefix of status.link must be in the format /dist/%s/", extensionName)
|
||||
return nil, fmt.Errorf("the prefix of status.link must be in the format /dist/%s", extensionName)
|
||||
}
|
||||
|
||||
if jsBundle.Spec.Assets.Style != nil && extensionName != "" &&
|
||||
!strings.HasPrefix(jsBundle.Spec.Assets.Style.Link, fmt.Sprintf("/dist/%s", extensionName)) {
|
||||
return nil, fmt.Errorf("the prefix of assets style.link with %s must be in the format /dist/%s", jsBundle.Spec.Assets.Style.Link, extensionName)
|
||||
}
|
||||
if jsBundle.Spec.Assets.Files != nil && extensionName != "" {
|
||||
for _, file := range jsBundle.Spec.Assets.Files {
|
||||
if !strings.HasPrefix(file.Link, fmt.Sprintf("/dist/%s", extensionName)) {
|
||||
return nil, fmt.Errorf("the prefix of assets file.link with %s must be in the format /dist/%s", file.Link, extensionName)
|
||||
}
|
||||
}
|
||||
}
|
||||
jsBundles := &extensionsv1alpha1.JSBundleList{}
|
||||
if err := r.Client.List(ctx, jsBundles, &client.ListOptions{}); err != nil {
|
||||
@@ -88,6 +100,21 @@ func (r *JSBundleWebhook) validateJSBundle(ctx context.Context, jsBundle *extens
|
||||
item.Status.Link == jsBundle.Status.Link {
|
||||
return nil, fmt.Errorf("JSBundle %s is already exists", jsBundle.Status.Link)
|
||||
}
|
||||
|
||||
if jsBundle.Spec.Assets.Style != nil && item.Spec.Assets.Style != nil && item.Name != jsBundle.Name &&
|
||||
item.Spec.Assets.Style.Link == jsBundle.Spec.Assets.Style.Link {
|
||||
return nil, fmt.Errorf("JSBundle asstes style %s is already exists", jsBundle.Spec.Assets.Style.Link)
|
||||
}
|
||||
|
||||
if jsBundle.Spec.Assets.Files != nil && item.Spec.Assets.Files != nil && item.Name != jsBundle.Name {
|
||||
for _, assetsFile := range jsBundle.Spec.Assets.Files {
|
||||
for _, itemFile := range item.Spec.Assets.Files {
|
||||
if assetsFile.Link == itemFile.Link {
|
||||
return nil, fmt.Errorf("JSBundle asstes file %s is already exists", assetsFile.Link)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,27 @@ type JSBundleSpec struct {
|
||||
Raw []byte `json:"raw,omitempty"`
|
||||
// +optional
|
||||
RawFrom RawFrom `json:"rawFrom,omitempty"`
|
||||
// +optional
|
||||
Assets Assets `json:"assets,omitempty"`
|
||||
}
|
||||
|
||||
type Assets struct {
|
||||
Style *AuxiliaryStyle `json:"style,omitempty"`
|
||||
Files []FileLocation `json:"files,omitempty"`
|
||||
}
|
||||
|
||||
type AuxiliaryStyle struct {
|
||||
Link string `json:"link,omitempty"`
|
||||
Endpoint `json:",inline"`
|
||||
}
|
||||
|
||||
type FileLocation struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Link string `json:"link,omitempty"`
|
||||
// Set the MIME Type of the file, if not specified, it will be provided by the content-type response header in the upstream service by default.
|
||||
// +optional
|
||||
MIMEType *string `json:"mimeType,omitempty"`
|
||||
Endpoint `json:",inline"`
|
||||
}
|
||||
|
||||
type RawFrom struct {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
// Code generated by controller-gen. DO NOT EDIT.
|
||||
|
||||
@@ -106,6 +107,49 @@ func (in *APIServiceStatus) DeepCopy() *APIServiceStatus {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Assets) DeepCopyInto(out *Assets) {
|
||||
*out = *in
|
||||
if in.Style != nil {
|
||||
in, out := &in.Style, &out.Style
|
||||
*out = new(AuxiliaryStyle)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Files != nil {
|
||||
in, out := &in.Files, &out.Files
|
||||
*out = make([]FileLocation, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Assets.
|
||||
func (in *Assets) DeepCopy() *Assets {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Assets)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *AuxiliaryStyle) DeepCopyInto(out *AuxiliaryStyle) {
|
||||
*out = *in
|
||||
in.Endpoint.DeepCopyInto(&out.Endpoint)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuxiliaryStyle.
|
||||
func (in *AuxiliaryStyle) DeepCopy() *AuxiliaryStyle {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(AuxiliaryStyle)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ConfigMapKeyRef) DeepCopyInto(out *ConfigMapKeyRef) {
|
||||
*out = *in
|
||||
@@ -295,6 +339,27 @@ func (in *ExtensionEntryStatus) DeepCopy() *ExtensionEntryStatus {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *FileLocation) DeepCopyInto(out *FileLocation) {
|
||||
*out = *in
|
||||
if in.MIMEType != nil {
|
||||
in, out := &in.MIMEType, &out.MIMEType
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
in.Endpoint.DeepCopyInto(&out.Endpoint)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileLocation.
|
||||
func (in *FileLocation) DeepCopy() *FileLocation {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FileLocation)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *JSBundle) DeepCopyInto(out *JSBundle) {
|
||||
*out = *in
|
||||
@@ -363,6 +428,7 @@ func (in *JSBundleSpec) DeepCopyInto(out *JSBundleSpec) {
|
||||
copy(*out, *in)
|
||||
}
|
||||
in.RawFrom.DeepCopyInto(&out.RawFrom)
|
||||
in.Assets.DeepCopyInto(&out.Assets)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JSBundleSpec.
|
||||
|
||||
Reference in New Issue
Block a user