diff --git a/README.md b/README.md
index 0c3e3b09..26054844 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,7 @@ Cyclops can either be installed manually by applying the latest manifest, by usi
To install Cyclops using `kubectl` into your cluster, run the commands below:
```bash
-kubectl apply -f https://raw.githubusercontent.com/cyclops-ui/cyclops/v0.11.1/install/cyclops-install.yaml && kubectl apply -f https://raw.githubusercontent.com/cyclops-ui/cyclops/v0.11.1/install/demo-templates.yaml
+kubectl apply -f https://raw.githubusercontent.com/cyclops-ui/cyclops/v0.12.0/install/cyclops-install.yaml && kubectl apply -f https://raw.githubusercontent.com/cyclops-ui/cyclops/v0.12.0/install/demo-templates.yaml
```
It will create a new namespace called `cyclops` and deploy everything you need for your Cyclops instance to run.
diff --git a/cyclops-ctrl/internal/controller/modules.go b/cyclops-ctrl/internal/controller/modules.go
index 23e972af..1017b9ea 100644
--- a/cyclops-ctrl/internal/controller/modules.go
+++ b/cyclops-ctrl/internal/controller/modules.go
@@ -158,6 +158,7 @@ func (m *Modules) Manifest(ctx *gin.Context) {
request.TemplateRef.URL,
request.TemplateRef.Path,
request.TemplateRef.Version,
+ "",
)
if err != nil {
fmt.Println(err)
@@ -204,6 +205,7 @@ func (m *Modules) CurrentManifest(ctx *gin.Context) {
module.Spec.TemplateRef.URL,
module.Spec.TemplateRef.Path,
module.Spec.TemplateRef.Version,
+ module.Status.TemplateResolvedVersion,
)
if err != nil {
fmt.Println(err)
@@ -396,6 +398,7 @@ func (m *Modules) ResourcesForModule(ctx *gin.Context) {
module.Spec.TemplateRef.URL,
module.Spec.TemplateRef.Path,
templateVersion,
+ module.Status.TemplateResolvedVersion,
)
if err != nil {
ctx.JSON(http.StatusInternalServerError, dto.NewError("Error fetching template", err.Error()))
@@ -440,6 +443,7 @@ func (m *Modules) Template(ctx *gin.Context) {
module.Spec.TemplateRef.URL,
module.Spec.TemplateRef.Path,
module.Spec.TemplateRef.Version,
+ module.Status.TemplateResolvedVersion,
)
if err != nil {
fmt.Println(err)
@@ -458,6 +462,7 @@ func (m *Modules) Template(ctx *gin.Context) {
module.Spec.TemplateRef.URL,
module.Spec.TemplateRef.Path,
module.Spec.TemplateRef.Version,
+ module.Status.TemplateResolvedVersion,
)
if err != nil {
fmt.Println(err)
@@ -494,6 +499,7 @@ func (m *Modules) HelmTemplate(ctx *gin.Context) {
module.Spec.TemplateRef.URL,
module.Spec.TemplateRef.Path,
module.Spec.TemplateRef.Version,
+ module.Status.TemplateResolvedVersion,
)
if err != nil {
fmt.Println(err)
diff --git a/cyclops-ctrl/internal/controller/sse/resources.go b/cyclops-ctrl/internal/controller/sse/resources.go
index 5fc4ea6f..29804b3e 100644
--- a/cyclops-ctrl/internal/controller/sse/resources.go
+++ b/cyclops-ctrl/internal/controller/sse/resources.go
@@ -5,11 +5,85 @@ import (
"net/http"
"time"
+ "github.com/pkg/errors"
+
"github.com/gin-gonic/gin"
"k8s.io/apimachinery/pkg/runtime/schema"
+
+ "github.com/cyclops-ui/cyclops/cyclops-ctrl/pkg/cluster/k8sclient"
)
func (s *Server) Resources(ctx *gin.Context) {
+ resources, err := s.k8sClient.GetWorkloadsForModule(ctx.Param("name"))
+ if err != nil {
+ ctx.String(http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ watchSpecs := make([]k8sclient.ResourceWatchSpec, 0, len(resources))
+ for _, resource := range resources {
+ if !k8sclient.IsWorkload(resource.GetGroup(), resource.GetVersion(), resource.GetKind()) {
+ continue
+ }
+
+ resourceName, err := kindToResource(resource.GetKind())
+ if err != nil {
+ ctx.String(http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ watchSpecs = append(watchSpecs, k8sclient.ResourceWatchSpec{
+ GVR: schema.GroupVersionResource{
+ Group: resource.GetGroup(),
+ Version: resource.GetVersion(),
+ Resource: resourceName,
+ },
+ Namespace: resource.GetNamespace(),
+ Name: resource.GetName(),
+ })
+ }
+
+ stopCh := make(chan struct{})
+
+ watchResource, err := s.k8sClient.WatchKubernetesResources(watchSpecs, stopCh)
+ if err != nil {
+ ctx.String(http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ ctx.Stream(func(w io.Writer) bool {
+ for {
+ select {
+ case u, ok := <-watchResource:
+ if !ok {
+ return false
+ }
+
+ res, err := s.k8sClient.GetResource(
+ u.GroupVersionKind().Group,
+ u.GroupVersionKind().Version,
+ u.GroupVersionKind().Kind,
+ u.GetName(),
+ u.GetNamespace(),
+ )
+ if err != nil {
+ continue
+ }
+
+ ctx.SSEvent("resource-update", res)
+ return true
+ case <-ctx.Request.Context().Done():
+ close(stopCh)
+ return false
+ case <-ctx.Done():
+ close(stopCh)
+ return false
+ }
+ }
+ })
+}
+
+func (s *Server) SingleResource(ctx *gin.Context) {
type Ref struct {
Group string `json:"group" form:"group"`
Version string `json:"version" form:"version"`
@@ -73,3 +147,16 @@ func (s *Server) Resources(ctx *gin.Context) {
}
})
}
+
+func kindToResource(kind string) (string, error) {
+ switch kind {
+ case "Deployment":
+ return "deployments", nil
+ case "StatefulSet":
+ return "statefulsets", nil
+ case "DaemonSet":
+ return "daemonsets", nil
+ default:
+ return "", errors.Errorf("kind %v is not a workload", kind)
+ }
+}
diff --git a/cyclops-ctrl/internal/controller/templates.go b/cyclops-ctrl/internal/controller/templates.go
index ed9004ae..95923179 100644
--- a/cyclops-ctrl/internal/controller/templates.go
+++ b/cyclops-ctrl/internal/controller/templates.go
@@ -59,7 +59,7 @@ func (c *Templates) GetTemplate(ctx *gin.Context) {
return
}
- t, err := c.templatesRepo.GetTemplate(repo, path, commit)
+ t, err := c.templatesRepo.GetTemplate(repo, path, commit, "")
if err != nil {
fmt.Println(err)
ctx.JSON(http.StatusBadRequest, dto.NewError("Error loading template", err.Error()))
@@ -131,7 +131,12 @@ func (c *Templates) CreateTemplatesStore(ctx *gin.Context) {
return
}
- tmpl, err := c.templatesRepo.GetTemplate(templateStore.TemplateRef.URL, templateStore.TemplateRef.Path, templateStore.TemplateRef.Version)
+ tmpl, err := c.templatesRepo.GetTemplate(
+ templateStore.TemplateRef.URL,
+ templateStore.TemplateRef.Path,
+ templateStore.TemplateRef.Version,
+ "",
+ )
if err != nil {
fmt.Println(err)
ctx.JSON(http.StatusBadRequest, dto.NewError("Error loading template", err.Error()))
@@ -169,7 +174,12 @@ func (c *Templates) EditTemplatesStore(ctx *gin.Context) {
return
}
- tmpl, err := c.templatesRepo.GetTemplate(templateStore.TemplateRef.URL, templateStore.TemplateRef.Path, templateStore.TemplateRef.Version)
+ tmpl, err := c.templatesRepo.GetTemplate(
+ templateStore.TemplateRef.URL,
+ templateStore.TemplateRef.Path,
+ templateStore.TemplateRef.Version,
+ "",
+ )
if err != nil {
fmt.Println(err)
ctx.JSON(http.StatusBadRequest, dto.NewError("Error loading template", err.Error()))
diff --git a/cyclops-ctrl/internal/handler/handler.go b/cyclops-ctrl/internal/handler/handler.go
index a425e9e4..47c6d8a9 100644
--- a/cyclops-ctrl/internal/handler/handler.go
+++ b/cyclops-ctrl/internal/handler/handler.go
@@ -52,7 +52,8 @@ func (h *Handler) Start() error {
server := sse.NewServer(h.k8sClient)
- h.router.POST("/stream/resources", sse.HeadersMiddleware(), server.Resources)
+ h.router.GET("/stream/resources/:name", sse.HeadersMiddleware(), server.Resources)
+ h.router.POST("/stream/resources", sse.HeadersMiddleware(), server.SingleResource)
h.router.GET("/ping", h.pong())
diff --git a/cyclops-ctrl/internal/mapper/modules.go b/cyclops-ctrl/internal/mapper/modules.go
index 189cf1cd..3581442a 100644
--- a/cyclops-ctrl/internal/mapper/modules.go
+++ b/cyclops-ctrl/internal/mapper/modules.go
@@ -2,6 +2,8 @@ package mapper
import (
"fmt"
+ v1 "k8s.io/api/core/v1"
+ "strings"
json "github.com/json-iterator/go"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@@ -27,7 +29,7 @@ func RequestToModule(req dto.Module) (cyclopsv1alpha1.Module, error) {
Name: req.Name,
},
Spec: cyclopsv1alpha1.ModuleSpec{
- TargetNamespace: req.Namespace,
+ TargetNamespace: mapTargetNamespace(req.Namespace),
TemplateRef: DtoTemplateRefToK8s(req.Template),
Values: apiextensionsv1.JSON{
Raw: data,
@@ -41,7 +43,7 @@ func ModuleToDTO(module cyclopsv1alpha1.Module) (dto.Module, error) {
return dto.Module{
Name: module.Name,
Namespace: module.Namespace,
- TargetNamespace: module.Spec.TargetNamespace,
+ TargetNamespace: mapTargetNamespace(module.Spec.TargetNamespace),
Version: module.Spec.TemplateRef.Version,
Template: k8sTemplateRefToDTO(module.Spec.TemplateRef, module.Status.TemplateResolvedVersion),
Values: module.Spec.Values,
@@ -139,3 +141,11 @@ func setValuesRecursive(moduleValues map[string]interface{}, fields map[string]m
return values, nil
}
+
+func mapTargetNamespace(targetNamespace string) string {
+ if len(strings.TrimSpace(targetNamespace)) == 0 {
+ return v1.NamespaceDefault
+ }
+
+ return targetNamespace
+}
diff --git a/cyclops-ctrl/internal/modulecontroller/module_controller.go b/cyclops-ctrl/internal/modulecontroller/module_controller.go
index 0841c2d0..38031365 100644
--- a/cyclops-ctrl/internal/modulecontroller/module_controller.go
+++ b/cyclops-ctrl/internal/modulecontroller/module_controller.go
@@ -133,6 +133,7 @@ func (r *ModuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
module.Spec.TemplateRef.URL,
module.Spec.TemplateRef.Path,
templateVersion,
+ module.Status.TemplateResolvedVersion,
)
if err != nil {
r.logger.Error(err, "error fetching module template", "namespaced name", req.NamespacedName)
diff --git a/cyclops-ctrl/internal/template/git.go b/cyclops-ctrl/internal/template/git.go
index 0e58a54a..76334b41 100644
--- a/cyclops-ctrl/internal/template/git.go
+++ b/cyclops-ctrl/internal/template/git.go
@@ -29,15 +29,20 @@ import (
"github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/template/gitproviders"
)
-func (r Repo) LoadTemplate(repoURL, path, commit string) (*models.Template, error) {
+func (r Repo) LoadTemplate(repoURL, path, commit, resolvedVersion string) (*models.Template, error) {
creds, err := r.credResolver.RepoAuthCredentials(repoURL)
if err != nil {
return nil, err
}
- commitSHA, err := resolveRef(repoURL, commit, creds)
- if err != nil {
- return nil, err
+ commitSHA := resolvedVersion
+ if len(commitSHA) == 0 {
+ ref, err := resolveRef(repoURL, commit, creds)
+ if err != nil {
+ return nil, err
+ }
+
+ commitSHA = ref
}
cached, ok := r.cache.GetTemplate(repoURL, path, commitSHA)
diff --git a/cyclops-ctrl/internal/template/helm.go b/cyclops-ctrl/internal/template/helm.go
index aab0a783..1c66121b 100644
--- a/cyclops-ctrl/internal/template/helm.go
+++ b/cyclops-ctrl/internal/template/helm.go
@@ -23,10 +23,12 @@ import (
"github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/models/helm"
)
-func (r Repo) LoadHelmChart(repo, chart, version string) (*models.Template, error) {
+func (r Repo) LoadHelmChart(repo, chart, version, resolvedVersion string) (*models.Template, error) {
var err error
strictVersion := version
- if !isValidVersion(version) {
+ if len(resolvedVersion) > 0 {
+ strictVersion = resolvedVersion
+ } else if !isValidVersion(version) {
if registry.IsOCI(repo) {
strictVersion, err = getOCIStrictVersion(repo, chart, version)
if err != nil {
diff --git a/cyclops-ctrl/internal/template/oci.go b/cyclops-ctrl/internal/template/oci.go
index 24e38c8c..4e46d133 100644
--- a/cyclops-ctrl/internal/template/oci.go
+++ b/cyclops-ctrl/internal/template/oci.go
@@ -13,10 +13,13 @@ import (
"github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/models"
)
-func (r Repo) LoadOCIHelmChart(repo, chart, version string) (*models.Template, error) {
+func (r Repo) LoadOCIHelmChart(repo, chart, version, resolvedVersion string) (*models.Template, error) {
var err error
strictVersion := version
- if !isValidVersion(version) {
+
+ if len(resolvedVersion) > 0 {
+ strictVersion = resolvedVersion
+ } else if !isValidVersion(version) {
strictVersion, err = getOCIStrictVersion(repo, chart, version)
if err != nil {
return nil, err
diff --git a/cyclops-ctrl/internal/template/template.go b/cyclops-ctrl/internal/template/template.go
index b6171172..3442987d 100644
--- a/cyclops-ctrl/internal/template/template.go
+++ b/cyclops-ctrl/internal/template/template.go
@@ -29,10 +29,10 @@ func NewRepo(credResolver auth.TemplatesResolver, tc templateCache) *Repo {
}
}
-func (r Repo) GetTemplate(repo, path, version string) (*models.Template, error) {
+func (r Repo) GetTemplate(repo, path, version, resolvedVersion string) (*models.Template, error) {
// region load OCI chart
if registry.IsOCI(repo) {
- return r.LoadOCIHelmChart(repo, path, version)
+ return r.LoadOCIHelmChart(repo, path, version, resolvedVersion)
}
// endregion
@@ -43,12 +43,12 @@ func (r Repo) GetTemplate(repo, path, version string) (*models.Template, error)
}
if isHelmRepo {
- return r.LoadHelmChart(repo, path, version)
+ return r.LoadHelmChart(repo, path, version, resolvedVersion)
}
// endregion
// fallback to cloning from git
- return r.LoadTemplate(repo, path, version)
+ return r.LoadTemplate(repo, path, version, resolvedVersion)
}
func (r Repo) GetTemplateInitialValues(repo, path, version string) (map[string]interface{}, error) {
@@ -80,7 +80,7 @@ func (r Repo) loadDependencies(metadata *helm.Metadata) ([]*models.Template, err
continue
}
- dep, err := r.GetTemplate(dependency.Repository, dependency.Name, dependency.Version)
+ dep, err := r.GetTemplate(dependency.Repository, dependency.Name, dependency.Version, "")
if err != nil {
return nil, err
}
diff --git a/cyclops-ctrl/pkg/cluster/k8sclient/client.go b/cyclops-ctrl/pkg/cluster/k8sclient/client.go
index 3b378853..f1523b0a 100644
--- a/cyclops-ctrl/pkg/cluster/k8sclient/client.go
+++ b/cyclops-ctrl/pkg/cluster/k8sclient/client.go
@@ -8,8 +8,10 @@ import (
"fmt"
"io"
networkingv1 "k8s.io/api/networking/v1"
+ "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/watch"
+ "k8s.io/client-go/tools/cache"
"os"
"os/exec"
"sort"
@@ -1112,6 +1114,12 @@ func isNetworkPolicy(group, version, kind string) bool {
return group == "networking.k8s.io" && version == "v1" && kind == "NetworkPolicy"
}
+func IsWorkload(group, version, kind string) bool {
+ return isDeployment(group, version, kind) ||
+ isStatefulSet(group, version, kind) ||
+ isDaemonSet(group, version, kind)
+}
+
func (k *KubernetesClient) WatchResource(group, version, resource, name, namespace string) (watch.Interface, error) {
gvr := schema.GroupVersionResource{
Group: group,
@@ -1123,3 +1131,58 @@ func (k *KubernetesClient) WatchResource(group, version, resource, name, namespa
FieldSelector: "metadata.name=" + name,
})
}
+
+type ResourceWatchSpec struct {
+ GVR schema.GroupVersionResource
+ Namespace string
+ Name string
+}
+
+func (k *KubernetesClient) WatchKubernetesResources(gvrs []ResourceWatchSpec, stopCh chan struct{}) (chan *unstructured.Unstructured, error) {
+ if len(gvrs) == 0 {
+ return nil, errors.New("no gvrs to watch")
+ }
+
+ eventChan := make(chan *unstructured.Unstructured, 1)
+
+ startWatch := func(spec ResourceWatchSpec) {
+ go func() {
+ resourceClient := k.Dynamic.Resource(spec.GVR).Namespace(spec.Namespace)
+
+ informer := cache.NewSharedInformer(
+ &cache.ListWatch{
+ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
+ options.FieldSelector = "metadata.name=" + spec.Name
+ return resourceClient.List(context.TODO(), options)
+ },
+ WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
+ options.FieldSelector = "metadata.name=" + spec.Name
+ return resourceClient.Watch(context.TODO(), options)
+ },
+ },
+ &unstructured.Unstructured{},
+ 0,
+ )
+
+ informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
+ AddFunc: func(obj interface{}) {
+ eventChan <- obj.(*unstructured.Unstructured)
+ },
+ UpdateFunc: func(oldObj, newObj interface{}) {
+ eventChan <- newObj.(*unstructured.Unstructured)
+ },
+ DeleteFunc: func(obj interface{}) {
+ eventChan <- obj.(*unstructured.Unstructured)
+ },
+ })
+
+ informer.Run(stopCh)
+ }()
+ }
+
+ for _, gvr := range gvrs {
+ startWatch(gvr)
+ }
+
+ return eventChan, nil
+}
diff --git a/cyclops-ctrl/pkg/cluster/k8sclient/modules.go b/cyclops-ctrl/pkg/cluster/k8sclient/modules.go
index a788d7a9..5bd75f78 100644
--- a/cyclops-ctrl/pkg/cluster/k8sclient/modules.go
+++ b/cyclops-ctrl/pkg/cluster/k8sclient/modules.go
@@ -2,6 +2,7 @@ package k8sclient
import (
"context"
+ "regexp"
"sort"
"strings"
@@ -106,6 +107,63 @@ func (k *KubernetesClient) GetResourcesForModule(name string) ([]dto.Resource, e
return out, nil
}
+func (k *KubernetesClient) GetWorkloadsForModule(name string) ([]dto.Resource, error) {
+ out := make([]dto.Resource, 0, 0)
+
+ deployments, err := k.clientset.AppsV1().Deployments("").List(context.Background(), metav1.ListOptions{
+ LabelSelector: "cyclops.module=" + name,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ for _, item := range deployments.Items {
+ out = append(out, &dto.Other{
+ Group: "apps",
+ Version: "v1",
+ Kind: "Deployment",
+ Name: item.Name,
+ Namespace: item.Namespace,
+ })
+ }
+
+ statefulset, err := k.clientset.AppsV1().StatefulSets("").List(context.Background(), metav1.ListOptions{
+ LabelSelector: "cyclops.module=" + name,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ for _, item := range statefulset.Items {
+ out = append(out, &dto.Other{
+ Group: "apps",
+ Version: "v1",
+ Kind: "StatefulSet",
+ Name: item.Name,
+ Namespace: item.Namespace,
+ })
+ }
+
+ daemonsets, err := k.clientset.AppsV1().DaemonSets("").List(context.Background(), metav1.ListOptions{
+ LabelSelector: "cyclops.module=" + name,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ for _, item := range daemonsets.Items {
+ out = append(out, &dto.Other{
+ Group: "apps",
+ Version: "v1",
+ Kind: "DaemonSet",
+ Name: item.Name,
+ Namespace: item.Namespace,
+ })
+ }
+
+ return out, nil
+}
+
func (k *KubernetesClient) getManagedGVRs(moduleName string) ([]schema.GroupVersionResource, error) {
module, _ := k.GetModule(moduleName)
@@ -242,7 +300,7 @@ func (k *KubernetesClient) GetModuleResourcesHealth(name string) (string, error)
resourcesWithHealth += len(deployments.Items)
for _, item := range deployments.Items {
- if isProgressing(item.Status.Conditions) {
+ if isDeploymentProgressing(item.Status.Conditions) {
return statusProgressing, nil
}
@@ -262,6 +320,10 @@ func (k *KubernetesClient) GetModuleResourcesHealth(name string) (string, error)
resourcesWithHealth += len(statefulsets.Items)
for _, item := range statefulsets.Items {
+ if isStatefulSetProgressing(item.Status, item.Spec.Replicas, item.Generation) {
+ return statusProgressing, nil
+ }
+
if item.Generation != item.Status.ObservedGeneration ||
item.Status.Replicas != item.Status.UpdatedReplicas ||
item.Status.Replicas != item.Status.AvailableReplicas {
@@ -420,10 +482,16 @@ func (k *KubernetesClient) getPods(deployment appsv1.Deployment) ([]dto.Pod, err
return nil, err
}
+ rs, singleRS := deploymentAvailableReplicaSet(deployment.Status.Conditions)
+
out := make([]dto.Pod, 0, len(pods.Items))
for _, item := range pods.Items {
containers := make([]dto.Container, 0, len(item.Spec.Containers))
+ if singleRS && len(rs) > 0 && !isPodOwner(item, rs) {
+ continue
+ }
+
for _, cnt := range item.Spec.Containers {
env := make(map[string]string)
for _, envVar := range cnt.Env {
@@ -757,7 +825,7 @@ func containerStatus(status apiv1.ContainerStatus) dto.ContainerStatus {
}
func getDeploymentStatus(deployment *appsv1.Deployment) string {
- if isProgressing(deployment.Status.Conditions) {
+ if isDeploymentProgressing(deployment.Status.Conditions) {
return statusProgressing
}
@@ -777,6 +845,10 @@ func getStatefulSetStatus(statefulset *appsv1.StatefulSet) string {
return statusHealthy
}
+ if isStatefulSetProgressing(statefulset.Status, statefulset.Spec.Replicas, statefulset.Generation) {
+ return statusProgressing
+ }
+
return statusUnhealthy
}
@@ -800,7 +872,34 @@ func getPodStatus(containers []dto.Container) bool {
return true
}
-func isProgressing(conditions []appsv1.DeploymentCondition) bool {
+func deploymentAvailableReplicaSet(conditions []appsv1.DeploymentCondition) (string, bool) {
+ replicaSetNamePattern := regexp.MustCompile(`ReplicaSet "(.+?)" has successfully progressed`)
+
+ for _, condition := range conditions {
+ if condition.Type == appsv1.DeploymentProgressing {
+ match := replicaSetNamePattern.FindStringSubmatch(condition.Message)
+ if len(match) > 1 {
+ return match[1], true
+ }
+ }
+ }
+
+ return "", false
+}
+
+func isPodOwner(pod apiv1.Pod, rsName string) bool {
+ for _, reference := range pod.OwnerReferences {
+ if reference.APIVersion == "apps/v1" &&
+ reference.Kind == "ReplicaSet" &&
+ reference.Name == rsName {
+ return true
+ }
+ }
+
+ return false
+}
+
+func isDeploymentProgressing(conditions []appsv1.DeploymentCondition) bool {
progressingReason := ""
availableReason := ""
@@ -823,3 +922,19 @@ func isProgressing(conditions []appsv1.DeploymentCondition) bool {
progressingReason == "FoundNewReplicaSet" ||
progressingReason == "ReplicaSetUpdated")
}
+
+func isStatefulSetProgressing(status appsv1.StatefulSetStatus, desiredReplicas *int32, generation int64) bool {
+ if status.ObservedGeneration == 0 || generation > status.ObservedGeneration {
+ return true
+ }
+
+ if status.CurrentRevision != status.UpdateRevision {
+ return true
+ }
+
+ if desiredReplicas == nil {
+ return false
+ }
+
+ return status.ReadyReplicas < *desiredReplicas || status.UpdatedReplicas < *desiredReplicas
+}
diff --git a/cyclops-ui/src/components/form/TemplateFormFields.tsx b/cyclops-ui/src/components/form/TemplateFormFields.tsx
index 48804658..09a15db9 100644
--- a/cyclops-ui/src/components/form/TemplateFormFields.tsx
+++ b/cyclops-ui/src/components/form/TemplateFormFields.tsx
@@ -74,6 +74,7 @@ export function mapFields(
formItemName={formItemName}
arrayField={arrayField}
isRequired={isRequired}
+ isModuleEdit={isModuleEdit}
/>,
);
return;
diff --git a/cyclops-ui/src/components/form/fields/string/SelectInput.tsx b/cyclops-ui/src/components/form/fields/string/SelectInput.tsx
index 5b09fae8..f1015fea 100644
--- a/cyclops-ui/src/components/form/fields/string/SelectInput.tsx
+++ b/cyclops-ui/src/components/form/fields/string/SelectInput.tsx
@@ -11,6 +11,7 @@ interface Props {
formItemName: string | string[];
arrayField: any;
isRequired: boolean;
+ isModuleEdit: boolean;
}
export const SelectInputField = ({
@@ -18,6 +19,7 @@ export const SelectInputField = ({
formItemName,
arrayField,
isRequired,
+ isModuleEdit,
}: Props) => {
const selectOptions = (field: any) => {
let options: Option[] = [];
@@ -62,6 +64,7 @@ export const SelectInputField = ({
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
options={selectOptions(field)}
+ disabled={field.immutable && isModuleEdit}
/>
);
diff --git a/cyclops-ui/src/components/k8s-resources/DaemonSet.tsx b/cyclops-ui/src/components/k8s-resources/DaemonSet.tsx
index 2c1f79f1..e31ab9a4 100644
--- a/cyclops-ui/src/components/k8s-resources/DaemonSet.tsx
+++ b/cyclops-ui/src/components/k8s-resources/DaemonSet.tsx
@@ -3,15 +3,15 @@ import { Col, Divider, Row, Alert } from "antd";
import axios from "axios";
import { mapResponseError } from "../../utils/api/errors";
import PodTable from "./common/PodTable/PodTable";
-import { resourceStream } from "../../utils/api/sse/resources";
import { isStreamingEnabled } from "../../utils/api/common";
interface Props {
name: string;
namespace: string;
+ workload: any;
}
-const DaemonSet = ({ name, namespace }: Props) => {
+const DaemonSet = ({ name, namespace, workload }: Props) => {
const [daemonSet, setDaemonSet] = useState({
status: "",
pods: [],
@@ -22,14 +22,6 @@ const DaemonSet = ({ name, namespace }: Props) => {
description: "",
});
- useEffect(() => {
- if (isStreamingEnabled()) {
- resourceStream(`apps`, `v1`, `DaemonSet`, name, namespace, (r: any) => {
- setDaemonSet(r);
- });
- }
- }, [name, namespace]);
-
const fetchDaemonSet = useCallback(() => {
axios
.get(`/api/resources`, {
@@ -53,7 +45,7 @@ const DaemonSet = ({ name, namespace }: Props) => {
fetchDaemonSet();
if (isStreamingEnabled()) {
- return () => {};
+ return;
}
const interval = setInterval(() => fetchDaemonSet(), 15000);
@@ -62,6 +54,24 @@ const DaemonSet = ({ name, namespace }: Props) => {
};
}, [fetchDaemonSet]);
+ function getPods() {
+ if (workload && isStreamingEnabled()) {
+ return workload.pods;
+ }
+
+ return daemonSet.pods;
+ }
+
+ function getPodsLength() {
+ let pods = getPods();
+
+ if (Array.isArray(pods)) {
+ return pods.length;
+ }
+
+ return 0;
+ }
+
return (
{error.message.length !== 0 && (
@@ -85,12 +95,12 @@ const DaemonSet = ({ name, namespace }: Props) => {
orientationMargin="0"
orientation={"left"}
>
- Pods: {daemonSet.pods.length}
+ Replicas: {getPodsLength()}
diff --git a/cyclops-ui/src/components/k8s-resources/Deployment.tsx b/cyclops-ui/src/components/k8s-resources/Deployment.tsx
index c4897309..e94cc92e 100644
--- a/cyclops-ui/src/components/k8s-resources/Deployment.tsx
+++ b/cyclops-ui/src/components/k8s-resources/Deployment.tsx
@@ -3,15 +3,15 @@ import { Col, Divider, Row, Alert } from "antd";
import axios from "axios";
import { mapResponseError } from "../../utils/api/errors";
import PodTable from "./common/PodTable/PodTable";
-import { resourceStream } from "../../utils/api/sse/resources";
import { isStreamingEnabled } from "../../utils/api/common";
interface Props {
name: string;
namespace: string;
+ workload: any;
}
-const Deployment = ({ name, namespace }: Props) => {
+const Deployment = ({ name, namespace, workload }: Props) => {
const [deployment, setDeployment] = useState({
status: "",
pods: [],
@@ -21,14 +21,6 @@ const Deployment = ({ name, namespace }: Props) => {
description: "",
});
- useEffect(() => {
- if (isStreamingEnabled()) {
- resourceStream(`apps`, `v1`, `Deployment`, name, namespace, (r: any) => {
- setDeployment(r);
- });
- }
- }, [name, namespace]);
-
const fetchDeployment = useCallback(() => {
axios
.get(`/api/resources`, {
@@ -52,7 +44,7 @@ const Deployment = ({ name, namespace }: Props) => {
fetchDeployment();
if (isStreamingEnabled()) {
- return () => {};
+ return;
}
const interval = setInterval(() => fetchDeployment(), 15000);
@@ -61,6 +53,24 @@ const Deployment = ({ name, namespace }: Props) => {
};
}, [fetchDeployment]);
+ function getPods() {
+ if (workload && isStreamingEnabled()) {
+ return workload.pods;
+ }
+
+ return deployment.pods;
+ }
+
+ function getPodsLength() {
+ let pods = getPods();
+
+ if (Array.isArray(pods)) {
+ return pods.length;
+ }
+
+ return 0;
+ }
+
return (
{error.message.length !== 0 && (
@@ -84,13 +94,13 @@ const Deployment = ({ name, namespace }: Props) => {
orientationMargin="0"
orientation={"left"}
>
- Replicas: {deployment.pods.length}
+ Replicas: {getPodsLength()}
{}}
/>
diff --git a/cyclops-ui/src/components/k8s-resources/StatefulSet.tsx b/cyclops-ui/src/components/k8s-resources/StatefulSet.tsx
index 8fab853e..a967cfc9 100644
--- a/cyclops-ui/src/components/k8s-resources/StatefulSet.tsx
+++ b/cyclops-ui/src/components/k8s-resources/StatefulSet.tsx
@@ -3,21 +3,16 @@ import { Col, Divider, Row, Alert } from "antd";
import axios from "axios";
import { mapResponseError } from "../../utils/api/errors";
import PodTable from "./common/PodTable/PodTable";
-import { resourceStream } from "../../utils/api/sse/resources";
import { isStreamingEnabled } from "../../utils/api/common";
interface Props {
name: string;
namespace: string;
+ workload: any;
}
-interface Statefulset {
- status: string;
- pods: any[];
-}
-
-const StatefulSet = ({ name, namespace }: Props) => {
- const [statefulSet, setStatefulSet] = useState({
+const StatefulSet = ({ name, namespace, workload }: Props) => {
+ const [statefulSet, setStatefulSet] = useState({
status: "",
pods: [],
});
@@ -27,14 +22,6 @@ const StatefulSet = ({ name, namespace }: Props) => {
description: "",
});
- useEffect(() => {
- if (isStreamingEnabled()) {
- resourceStream(`apps`, `v1`, `StatefulSet`, name, namespace, (r: any) => {
- setStatefulSet(r);
- });
- }
- }, [name, namespace]);
-
const fetchStatefulSet = useCallback(() => {
axios
.get(`/api/resources`, {
@@ -58,7 +45,7 @@ const StatefulSet = ({ name, namespace }: Props) => {
fetchStatefulSet();
if (isStreamingEnabled()) {
- return () => {};
+ return;
}
const interval = setInterval(() => fetchStatefulSet(), 15000);
@@ -67,6 +54,24 @@ const StatefulSet = ({ name, namespace }: Props) => {
};
}, [fetchStatefulSet]);
+ function getPods() {
+ if (workload && isStreamingEnabled()) {
+ return workload.pods;
+ }
+
+ return statefulSet.pods;
+ }
+
+ function getPodsLength() {
+ let pods = getPods();
+
+ if (Array.isArray(pods)) {
+ return pods.length;
+ }
+
+ return 0;
+ }
+
return (
{error.message.length !== 0 && (
@@ -90,13 +95,13 @@ const StatefulSet = ({ name, namespace }: Props) => {
orientationMargin="0"
orientation={"left"}
>
- Replicas: {statefulSet.pods.length}
+ Replicas: {getPodsLength()}
{}}
/>
diff --git a/cyclops-ui/src/components/pages/EditModule/EditModule.tsx b/cyclops-ui/src/components/pages/EditModule/EditModule.tsx
index b86f6955..be514d8a 100644
--- a/cyclops-ui/src/components/pages/EditModule/EditModule.tsx
+++ b/cyclops-ui/src/components/pages/EditModule/EditModule.tsx
@@ -12,7 +12,6 @@ import {
Typography,
} from "antd";
import axios from "axios";
-import { useNavigate } from "react-router";
import { LockFilled, UnlockFilled } from "@ant-design/icons";
import { useParams } from "react-router-dom";
@@ -101,8 +100,6 @@ const EditModule = () => {
const [loadValues, setLoadValues] = useState(false);
const [loadTemplate, setLoadTemplate] = useState(false);
- const history = useNavigate();
-
let { moduleName } = useParams();
const mapsToArray = useCallback((fields: any[], values: any): any => {
@@ -410,7 +407,7 @@ const EditModule = () => {
{" "}
+ }
+ style={{ padding: "0px 12px 0px 12px" }}
+ hasFeedback={true}
+ validateDebounce={1000}
+ >
+
+
+
+
+ {advancedOptionsExpanded ? (
- Target namespace
-
- Namespace used to deploy resources to
-
+ Advanced
+
- }
- hasFeedback={true}
- validateDebounce={1000}
- >
-
-
+ ) : (
+
+ Advanced
+
+
+ )}
+
{
{" "}
history("/")}
+ onClick={() => (window.location.href = "/")}
disabled={loadingTemplate || loadingTemplateInitialValues}
>
Back
diff --git a/cyclops-ui/src/components/pages/NewModule/custom.css b/cyclops-ui/src/components/pages/NewModule/custom.css
index b1e656c3..0596d983 100644
--- a/cyclops-ui/src/components/pages/NewModule/custom.css
+++ b/cyclops-ui/src/components/pages/NewModule/custom.css
@@ -26,3 +26,20 @@ code {
padding: 0;
}
}
+
+.expandadvanced {
+ width: 100%;
+ height: 32px;
+ background-color: #eee;
+ border-radius: 0 0 7px 7px;
+ transition: background-color 0.2s ease;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ cursor: pointer;
+}
+
+.expandadvanced:hover {
+ background-color: #e0e0e0;
+}
diff --git a/cyclops-ui/src/static/img/default-template-icon.png b/cyclops-ui/src/static/img/default-template-icon.png
index 0d4f3c81..86977150 100644
Binary files a/cyclops-ui/src/static/img/default-template-icon.png and b/cyclops-ui/src/static/img/default-template-icon.png differ
diff --git a/cyclops-ui/src/utils/api/sse/resources.tsx b/cyclops-ui/src/utils/api/sse/resources.tsx
index 5e681993..38b7ee84 100644
--- a/cyclops-ui/src/utils/api/sse/resources.tsx
+++ b/cyclops-ui/src/utils/api/sse/resources.tsx
@@ -54,3 +54,45 @@ export function resourceStream(
},
}).catch((r) => console.error(r));
}
+
+export function resourcesStream(
+ moduleName: string | undefined,
+ setResource: (r: any) => void,
+) {
+ if (!moduleName) {
+ return;
+ }
+
+ fetchEventSource(`/api/stream/resources/` + moduleName, {
+ method: "GET",
+ onmessage(ev) {
+ setResource(JSON.parse(ev.data));
+ },
+ async onopen(response) {
+ if (
+ response.ok &&
+ response.headers.get("content-type") === EventStreamContentType
+ ) {
+ return;
+ } else if (
+ response.status >= 400 &&
+ response.status < 500 &&
+ response.status !== 429
+ ) {
+ throw new FatalError();
+ } else {
+ throw new RetriableError();
+ }
+ },
+ onclose() {
+ throw new RetriableError();
+ },
+ onerror: (err) => {
+ if (err instanceof FatalError) {
+ throw err; // rethrow to stop the operation
+ }
+
+ return 5000;
+ },
+ }).catch((r) => console.error(r));
+}
diff --git a/cyclops-ui/src/utils/resourceRef.tsx b/cyclops-ui/src/utils/resourceRef.tsx
new file mode 100644
index 00000000..dad6f105
--- /dev/null
+++ b/cyclops-ui/src/utils/resourceRef.tsx
@@ -0,0 +1,27 @@
+export interface ResourceRef {
+ group: string;
+ version: string;
+ kind: string;
+ name: string;
+ namespace: string;
+}
+
+export function resourceRefKey(r: ResourceRef): string {
+ return `${r.group}/${r.version}/${r.kind}/${r.namespace}/${r.name}`;
+}
+
+export function isWorkload(r: ResourceRef): boolean {
+ return isDeployment(r) || isStatefulSet(r) || isDaemonSet(r);
+}
+
+export function isDeployment(r: ResourceRef): boolean {
+ return r.group === "apps" && r.version === "v1" && r.kind === "Deployment";
+}
+
+export function isStatefulSet(r: ResourceRef): boolean {
+ return r.group === "apps" && r.version === "v1" && r.kind === "StatefulSet";
+}
+
+export function isDaemonSet(r: ResourceRef): boolean {
+ return r.group === "apps" && r.version === "v1" && r.kind === "DaemonSet";
+}
diff --git a/cyclops-ui/yarn.lock b/cyclops-ui/yarn.lock
index a4899b7d..746889e5 100644
--- a/cyclops-ui/yarn.lock
+++ b/cyclops-ui/yarn.lock
@@ -5075,7 +5075,25 @@ bluebird@^3.7.2:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
-body-parser@1.20.2, body-parser@^1.19.0:
+body-parser@1.20.3:
+ version "1.20.3"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6"
+ integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==
+ dependencies:
+ bytes "3.1.2"
+ content-type "~1.0.5"
+ debug "2.6.9"
+ depd "2.0.0"
+ destroy "1.2.0"
+ http-errors "2.0.0"
+ iconv-lite "0.4.24"
+ on-finished "2.4.1"
+ qs "6.13.0"
+ raw-body "2.5.2"
+ type-is "~1.6.18"
+ unpipe "1.0.0"
+
+body-parser@^1.19.0:
version "1.20.2"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==
@@ -6674,6 +6692,11 @@ encodeurl@~1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
+encodeurl@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
+ integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
+
enhanced-resolve@^5.15.0:
version "5.16.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz#65ec88778083056cb32487faa9aef82ed0864787"
@@ -7356,36 +7379,36 @@ expect@^29.0.0:
jest-util "^29.7.0"
express@^4.17.1, express@^4.17.3:
- version "4.19.2"
- resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
- integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==
+ version "4.21.0"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.21.0.tgz#d57cb706d49623d4ac27833f1cbc466b668eb915"
+ integrity sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==
dependencies:
accepts "~1.3.8"
array-flatten "1.1.1"
- body-parser "1.20.2"
+ body-parser "1.20.3"
content-disposition "0.5.4"
content-type "~1.0.4"
cookie "0.6.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "2.0.0"
- encodeurl "~1.0.2"
+ encodeurl "~2.0.0"
escape-html "~1.0.3"
etag "~1.8.1"
- finalhandler "1.2.0"
+ finalhandler "1.3.1"
fresh "0.5.2"
http-errors "2.0.0"
- merge-descriptors "1.0.1"
+ merge-descriptors "1.0.3"
methods "~1.1.2"
on-finished "2.4.1"
parseurl "~1.3.3"
- path-to-regexp "0.1.7"
+ path-to-regexp "0.1.10"
proxy-addr "~2.0.7"
- qs "6.11.0"
+ qs "6.13.0"
range-parser "~1.2.1"
safe-buffer "5.2.1"
- send "0.18.0"
- serve-static "1.15.0"
+ send "0.19.0"
+ serve-static "1.16.2"
setprototypeof "1.2.0"
statuses "2.0.1"
type-is "~1.6.18"
@@ -7560,13 +7583,13 @@ fill-range@^7.1.1:
dependencies:
to-regex-range "^5.0.1"
-finalhandler@1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32"
- integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==
+finalhandler@1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019"
+ integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==
dependencies:
debug "2.6.9"
- encodeurl "~1.0.2"
+ encodeurl "~2.0.0"
escape-html "~1.0.3"
on-finished "2.4.1"
parseurl "~1.3.3"
@@ -10030,10 +10053,10 @@ memoize-one@^5.0.4:
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
-merge-descriptors@1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
- integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==
+merge-descriptors@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5"
+ integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==
merge-source-map@1.0.4:
version "1.0.4"
@@ -10712,10 +10735,10 @@ path-scurry@^1.10.1:
lru-cache "^9.1.1 || ^10.0.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
-path-to-regexp@0.1.7:
- version "0.1.7"
- resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
- integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
+path-to-regexp@0.1.10:
+ version "0.1.10"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b"
+ integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==
path-type@^4.0.0:
version "4.0.0"
@@ -11571,6 +11594,13 @@ qs@6.11.0:
dependencies:
side-channel "^1.0.4"
+qs@6.13.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
+ integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==
+ dependencies:
+ side-channel "^1.0.6"
+
querystringify@^2.1.1:
version "2.2.0"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
@@ -12922,10 +12952,10 @@ semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.5.3, semver@^7.5.4:
dependencies:
lru-cache "^6.0.0"
-send@0.18.0:
- version "0.18.0"
- resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
- integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==
+send@0.19.0:
+ version "0.19.0"
+ resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8"
+ integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==
dependencies:
debug "2.6.9"
depd "2.0.0"
@@ -12968,15 +12998,15 @@ serve-index@^1.9.1:
mime-types "~2.1.17"
parseurl "~1.3.2"
-serve-static@1.15.0:
- version "1.15.0"
- resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540"
- integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==
+serve-static@1.16.2:
+ version "1.16.2"
+ resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296"
+ integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==
dependencies:
- encodeurl "~1.0.2"
+ encodeurl "~2.0.0"
escape-html "~1.0.3"
parseurl "~1.3.3"
- send "0.18.0"
+ send "0.19.0"
set-blocking@^2.0.0:
version "2.0.0"
@@ -13062,7 +13092,7 @@ shell-quote@^1.7.3, shell-quote@^1.8.1:
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680"
integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==
-side-channel@^1.0.4:
+side-channel@^1.0.4, side-channel@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==
@@ -13406,7 +13436,16 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
-"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0":
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -13498,7 +13537,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -13512,6 +13551,13 @@ strip-ansi@^3.0.0:
dependencies:
ansi-regex "^2.0.0"
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
strip-ansi@^7.0.1, strip-ansi@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -14839,7 +14885,7 @@ workbox-window@6.6.1:
"@types/trusted-types" "^2.0.2"
workbox-core "6.6.1"
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -14857,6 +14903,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
+wrap-ansi@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
diff --git a/install/cyclops-install.yaml b/install/cyclops-install.yaml
index 5a71a378..231ab377 100644
--- a/install/cyclops-install.yaml
+++ b/install/cyclops-install.yaml
@@ -341,7 +341,7 @@ spec:
spec:
containers:
- name: cyclops-ui
- image: cyclopsui/cyclops-ui:v0.11.1
+ image: cyclopsui/cyclops-ui:v0.13.0
ports:
- containerPort: 80
env:
@@ -406,7 +406,7 @@ spec:
serviceAccountName: cyclops-ctrl
containers:
- name: cyclops-ctrl
- image: cyclopsui/cyclops-ctrl:v0.11.1
+ image: cyclopsui/cyclops-ctrl:v0.13.0
ports:
- containerPort: 8080
env:
diff --git a/web/docs/installation/install/manifest.md b/web/docs/installation/install/manifest.md
index 9176b216..eb1acbf9 100644
--- a/web/docs/installation/install/manifest.md
+++ b/web/docs/installation/install/manifest.md
@@ -7,7 +7,7 @@ sidebar_label: Using kubectl
To install Cyclops in your cluster, run commands below:
```bash
-kubectl apply -f https://raw.githubusercontent.com/cyclops-ui/cyclops/v0.11.1/install/cyclops-install.yaml && kubectl apply -f https://raw.githubusercontent.com/cyclops-ui/cyclops/v0.11.1/install/demo-templates.yaml
+kubectl apply -f https://raw.githubusercontent.com/cyclops-ui/cyclops/v0.12.0/install/cyclops-install.yaml && kubectl apply -f https://raw.githubusercontent.com/cyclops-ui/cyclops/v0.12.0/install/demo-templates.yaml
```
It will create a new namespace called `cyclops` and deploy everything you need for your Cyclops instance to run.
diff --git a/web/docs/templates/templates.md b/web/docs/templates/templates.md
index 5a28a1a1..76f30770 100644
--- a/web/docs/templates/templates.md
+++ b/web/docs/templates/templates.md
@@ -17,6 +17,8 @@ You can find a list of all the fields you can set below for each field type.
In addition to the usual schema, we added more fields to help our users get as much from the UI as possible.
| Name | Type | Description | Valid input |
-| :-------------- | ------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
+|:----------------|--------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------|
| `order` | string array | Defines the order of the fields in an object type property.
Each time you use `properties`, you should also define the order of those properties | - |
| `fileExtension` | string | Sometimes, you would like your text field not just to be a field but also to get some highlighting based on the type of string you are saving. You can specify that in this field | `text`, `sh`, `json`, `yaml`, `toml`, `javascript`, `typescript` |
+| `immutable` | boolean | If `true`, the field can't be updated through the UI when __editing__ a module. Can be edited when the Module is first created or via manifest in the cluster. | `true`, `false` (`false` by default) |
+
diff --git a/web/src/components/Install/Install/index.js b/web/src/components/Install/Install/index.js
index 1577bdaa..df6e7f3f 100644
--- a/web/src/components/Install/Install/index.js
+++ b/web/src/components/Install/Install/index.js
@@ -8,8 +8,8 @@ const InstallCmd = () => {
Install it with a single command
- {"kubectl apply -f https://raw.githubusercontent.com/cyclops-ui/cyclops/v0.11.1/install/cyclops-install.yaml && \n" +
- "kubectl apply -f https://raw.githubusercontent.com/cyclops-ui/cyclops/v0.11.1/install/demo-templates.yaml"}
+ {"kubectl apply -f https://raw.githubusercontent.com/cyclops-ui/cyclops/v0.12.0/install/cyclops-install.yaml && \n" +
+ "kubectl apply -f https://raw.githubusercontent.com/cyclops-ui/cyclops/v0.12.0/install/demo-templates.yaml"}
);