Add support for a per component pod status map (#50)

pull/4881/head
Periklis Tsirakidis 4 years ago committed by GitHub
parent d643cb2f4b
commit 6a12aef097
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 49
      api/v1beta1/lokistack_types.go
  2. 120
      api/v1beta1/zz_generated.deepcopy.go
  3. 25
      bundle/manifests/loki-operator.clusterserviceversion.yaml
  4. 39
      bundle/manifests/loki.openshift.io_lokistacks.yaml
  5. 45
      config/crd/bases/loki.openshift.io_lokistacks.yaml
  6. 25
      config/manifests/bases/loki-operator.clusterserviceversion.yaml
  7. 71
      controllers/internal/status/components.go
  8. 28
      controllers/lokistack_controller.go
  9. 10
      controllers/lokistack_controller_test.go
  10. 4
      internal/manifests/compactor.go
  11. 4
      internal/manifests/distributor.go
  12. 4
      internal/manifests/ingester.go
  13. 4
      internal/manifests/querier.go
  14. 4
      internal/manifests/query-frontend.go
  15. 38
      internal/manifests/var.go

@ -361,8 +361,57 @@ const (
ReasonInvalidReplicationConfiguration LokiStackConditionReason = "InvalidReplicationConfiguration"
)
// PodStatusMap defines the type for mapping pod status to pod name.
type PodStatusMap map[corev1.PodPhase][]string
// LokiStackComponentStatus defines the map of per pod status per LokiStack component.
// Each component is represented by a separate map of v1.Phase to a list of pods.
type LokiStackComponentStatus struct {
// Compactor is a map to the pod status of the compactor pod.
//
// +optional
// +kubebuilder:validation:Optional
// +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors="urn:alm:descriptor:com.tectonic.ui:podStatuses",displayName="Compactor",order=5
Compactor PodStatusMap `json:"compactor,omitempty"`
// Distributor is a map to the per pod status of the distributor deployment
//
// +optional
// +kubebuilder:validation:Optional
// +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors="urn:alm:descriptor:com.tectonic.ui:podStatuses",displayName="Distributor",order=1
Distributor PodStatusMap `json:"distributor,omitempty"`
// Ingester is a map to the per pod status of the ingester statefulset
//
// +optional
// +kubebuilder:validation:Optional
// +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors="urn:alm:descriptor:com.tectonic.ui:podStatuses",displayName="Ingester",order=2
Ingester PodStatusMap `json:"ingester,omitempty"`
// Querier is a map to the per pod status of the querier statefulset
//
// +optional
// +kubebuilder:validation:Optional
// +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors="urn:alm:descriptor:com.tectonic.ui:podStatuses",displayName="Querier",order=3
Querier PodStatusMap `json:"querier,omitempty"`
// QueryFrontend is a mpa to the per pod status of the query frontend deployment.
//
// +optional
// +kubebuilder:validation:Optional
// +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors="urn:alm:descriptor:com.tectonic.ui:podStatuses",displayName="Query Frontend",order=4
QueryFrontend PodStatusMap `json:"queryFrontend,omitempty"`
}
// LokiStackStatus defines the observed state of LokiStack
type LokiStackStatus struct {
// Components provides summary of all Loki pod status grouped
// per component.
//
// +optional
// +kubebuilder:validation:Optional
Components LokiStackComponentStatus `json:"components,omitempty"`
// Conditions of the Loki deployment health.
//
// +optional

@ -149,6 +149,96 @@ func (in *LokiStack) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LokiStackComponentStatus) DeepCopyInto(out *LokiStackComponentStatus) {
*out = *in
if in.Compactor != nil {
in, out := &in.Compactor, &out.Compactor
*out = make(PodStatusMap, len(*in))
for key, val := range *in {
var outVal []string
if val == nil {
(*out)[key] = nil
} else {
in, out := &val, &outVal
*out = make([]string, len(*in))
copy(*out, *in)
}
(*out)[key] = outVal
}
}
if in.Distributor != nil {
in, out := &in.Distributor, &out.Distributor
*out = make(PodStatusMap, len(*in))
for key, val := range *in {
var outVal []string
if val == nil {
(*out)[key] = nil
} else {
in, out := &val, &outVal
*out = make([]string, len(*in))
copy(*out, *in)
}
(*out)[key] = outVal
}
}
if in.Ingester != nil {
in, out := &in.Ingester, &out.Ingester
*out = make(PodStatusMap, len(*in))
for key, val := range *in {
var outVal []string
if val == nil {
(*out)[key] = nil
} else {
in, out := &val, &outVal
*out = make([]string, len(*in))
copy(*out, *in)
}
(*out)[key] = outVal
}
}
if in.Querier != nil {
in, out := &in.Querier, &out.Querier
*out = make(PodStatusMap, len(*in))
for key, val := range *in {
var outVal []string
if val == nil {
(*out)[key] = nil
} else {
in, out := &val, &outVal
*out = make([]string, len(*in))
copy(*out, *in)
}
(*out)[key] = outVal
}
}
if in.QueryFrontend != nil {
in, out := &in.QueryFrontend, &out.QueryFrontend
*out = make(PodStatusMap, len(*in))
for key, val := range *in {
var outVal []string
if val == nil {
(*out)[key] = nil
} else {
in, out := &val, &outVal
*out = make([]string, len(*in))
copy(*out, *in)
}
(*out)[key] = outVal
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LokiStackComponentStatus.
func (in *LokiStackComponentStatus) DeepCopy() *LokiStackComponentStatus {
if in == nil {
return nil
}
out := new(LokiStackComponentStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LokiStackList) DeepCopyInto(out *LokiStackList) {
*out = *in
@ -210,6 +300,7 @@ func (in *LokiStackSpec) DeepCopy() *LokiStackSpec {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LokiStackStatus) DeepCopyInto(out *LokiStackStatus) {
*out = *in
in.Components.DeepCopyInto(&out.Components)
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]metav1.Condition, len(*in))
@ -300,6 +391,35 @@ func (in *ObjectStorageSpec) DeepCopy() *ObjectStorageSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in PodStatusMap) DeepCopyInto(out *PodStatusMap) {
{
in := &in
*out = make(PodStatusMap, len(*in))
for key, val := range *in {
var outVal []string
if val == nil {
(*out)[key] = nil
} else {
in, out := &val, &outVal
*out = make([]string, len(*in))
copy(*out, *in)
}
(*out)[key] = outVal
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodStatusMap.
func (in PodStatusMap) DeepCopy() PodStatusMap {
if in == nil {
return nil
}
out := new(PodStatusMap)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *QueryLimitSpec) DeepCopyInto(out *QueryLimitSpec) {
*out = *in

@ -260,6 +260,31 @@ spec:
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:hidden
statusDescriptors:
- description: Distributor is a map to the per pod status of the distributor deployment
displayName: Distributor
path: components.distributor
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:podStatuses
- description: Ingester is a map to the per pod status of the ingester statefulset
displayName: Ingester
path: components.ingester
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:podStatuses
- description: Querier is a map to the per pod status of the querier statefulset
displayName: Querier
path: components.querier
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:podStatuses
- description: QueryFrontend is a mpa to the per pod status of the query frontend deployment.
displayName: Query Frontend
path: components.queryFrontend
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:podStatuses
- description: Compactor is a map to the pod status of the compactor pod.
displayName: Compactor
path: components.compactor
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:podStatuses
- description: Conditions of the Loki deployment health.
displayName: Conditions
path: conditions

@ -379,6 +379,45 @@ spec:
status:
description: LokiStackStatus defines the observed state of LokiStack
properties:
components:
description: Components provides summary of all Loki pod status grouped per component.
properties:
compactor:
additionalProperties:
items:
type: string
type: array
description: Compactor is a map to the pod status of the compactor pod.
type: object
distributor:
additionalProperties:
items:
type: string
type: array
description: Distributor is a map to the per pod status of the distributor deployment
type: object
ingester:
additionalProperties:
items:
type: string
type: array
description: Ingester is a map to the per pod status of the ingester statefulset
type: object
querier:
additionalProperties:
items:
type: string
type: array
description: Querier is a map to the per pod status of the querier statefulset
type: object
queryFrontend:
additionalProperties:
items:
type: string
type: array
description: QueryFrontend is a mpa to the per pod status of the query frontend deployment.
type: object
type: object
conditions:
description: Conditions of the Loki deployment health.
items:

@ -531,6 +531,51 @@ spec:
status:
description: LokiStackStatus defines the observed state of LokiStack
properties:
components:
description: Components provides summary of all Loki pod status grouped
per component.
properties:
compactor:
additionalProperties:
items:
type: string
type: array
description: Compactor is a map to the pod status of the compactor
pod.
type: object
distributor:
additionalProperties:
items:
type: string
type: array
description: Distributor is a map to the per pod status of the
distributor deployment
type: object
ingester:
additionalProperties:
items:
type: string
type: array
description: Ingester is a map to the per pod status of the ingester
statefulset
type: object
querier:
additionalProperties:
items:
type: string
type: array
description: Querier is a map to the per pod status of the querier
statefulset
type: object
queryFrontend:
additionalProperties:
items:
type: string
type: array
description: QueryFrontend is a mpa to the per pod status of the
query frontend deployment.
type: object
type: object
conditions:
description: Conditions of the Loki deployment health.
items:

@ -239,6 +239,31 @@ spec:
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:hidden
statusDescriptors:
- description: Distributor is a map to the per pod status of the distributor deployment
displayName: Distributor
path: components.distributor
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:podStatuses
- description: Ingester is a map to the per pod status of the ingester statefulset
displayName: Ingester
path: components.ingester
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:podStatuses
- description: Querier is a map to the per pod status of the querier statefulset
displayName: Querier
path: components.querier
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:podStatuses
- description: QueryFrontend is a mpa to the per pod status of the query frontend deployment.
displayName: Query Frontend
path: components.queryFrontend
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:podStatuses
- description: Compactor is a map to the pod status of the compactor pod.
displayName: Compactor
path: components.compactor
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:podStatuses
- description: Conditions of the Loki deployment health.
displayName: Conditions
path: conditions

@ -0,0 +1,71 @@
package status
import (
"context"
"github.com/ViaQ/logerr/kverrors"
lokiv1beta1 "github.com/ViaQ/loki-operator/api/v1beta1"
"github.com/ViaQ/loki-operator/internal/external/k8s"
"github.com/ViaQ/loki-operator/internal/manifests"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// SetComponentsStatus updates the pod status map component
func SetComponentsStatus(ctx context.Context, k k8s.Client, req ctrl.Request) error {
var s lokiv1beta1.LokiStack
if err := k.Get(ctx, req.NamespacedName, &s); err != nil {
if apierrors.IsNotFound(err) {
return nil
}
return kverrors.Wrap(err, "failed to lookup lokistack", "name", req.NamespacedName)
}
var err error
s.Status.Components = lokiv1beta1.LokiStackComponentStatus{}
s.Status.Components.Compactor, err = appendPodStatus(ctx, k, manifests.LabelCompactorComponent, s.Name, s.Namespace)
if err != nil {
return kverrors.Wrap(err, "failed lookup LokiStack component pods status", "name", manifests.LabelCompactorComponent)
}
s.Status.Components.Querier, err = appendPodStatus(ctx, k, manifests.LabelQuerierComponent, s.Name, s.Namespace)
if err != nil {
return kverrors.Wrap(err, "failed lookup LokiStack component pods status", "name", manifests.LabelQuerierComponent)
}
s.Status.Components.Distributor, err = appendPodStatus(ctx, k, manifests.LabelDistributorComponent, s.Name, s.Namespace)
if err != nil {
return kverrors.Wrap(err, "failed lookup LokiStack component pods status", "name", manifests.LabelDistributorComponent)
}
s.Status.Components.QueryFrontend, err = appendPodStatus(ctx, k, manifests.LabelQueryFrontendComponent, s.Name, s.Namespace)
if err != nil {
return kverrors.Wrap(err, "failed lookup LokiStack component pods status", "name", manifests.LabelQueryFrontendComponent)
}
s.Status.Components.Ingester, err = appendPodStatus(ctx, k, manifests.LabelIngesterComponent, s.Name, s.Namespace)
if err != nil {
return kverrors.Wrap(err, "failed lookup LokiStack component pods status", "name", manifests.LabelIngesterComponent)
}
return k.Status().Update(ctx, &s, &client.UpdateOptions{})
}
func appendPodStatus(ctx context.Context, k k8s.Client, component, stack, ns string) (lokiv1beta1.PodStatusMap, error) {
psm := lokiv1beta1.PodStatusMap{}
pods := &corev1.PodList{}
opts := []client.ListOption{
client.MatchingLabels(manifests.ComponentLabels(component, stack)),
client.InNamespace(ns),
}
if err := k.List(ctx, pods, opts...); err != nil {
return nil, kverrors.Wrap(err, "failed to list pods for LokiStack component", "name", stack, "component", component)
}
for _, pod := range pods.Items {
phase := pod.Status.Phase
psm[phase] = append(psm[phase], pod.Name)
}
return psm, nil
}

@ -21,6 +21,7 @@ import (
"time"
"github.com/ViaQ/loki-operator/controllers/internal/management/state"
"github.com/ViaQ/loki-operator/controllers/internal/status"
"github.com/ViaQ/loki-operator/internal/external/k8s"
"github.com/ViaQ/loki-operator/internal/handlers"
"github.com/go-logr/logr"
@ -52,8 +53,15 @@ var (
DeleteFunc: func(e event.DeleteEvent) bool { return false },
GenericFunc: func(e event.GenericEvent) bool { return false },
})
deleteOnlyPred = builder.WithPredicates(predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool { return false },
updateOrDeleteOnlyPred = builder.WithPredicates(predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
switch e.ObjectOld.(type) {
case *appsv1.Deployment:
case *appsv1.StatefulSet:
return true
}
return false
},
CreateFunc: func(e event.CreateEvent) bool { return false },
DeleteFunc: func(e event.DeleteEvent) bool {
// DeleteStateUnknown evaluates to false only if the object
@ -109,6 +117,14 @@ func (r *LokiStackReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
}, err
}
err = status.SetComponentsStatus(ctx, r.Client, req)
if err != nil {
return ctrl.Result{
Requeue: true,
RequeueAfter: time.Second,
}, err
}
return ctrl.Result{}, nil
}
@ -121,9 +137,9 @@ func (r *LokiStackReconciler) SetupWithManager(mgr manager.Manager) error {
func (r *LokiStackReconciler) buildController(bld k8s.Builder) error {
return bld.
For(&lokiv1beta1.LokiStack{}, createOrUpdateOnlyPred).
Owns(&corev1.ConfigMap{}, deleteOnlyPred).
Owns(&corev1.Service{}, deleteOnlyPred).
Owns(&appsv1.Deployment{}, deleteOnlyPred).
Owns(&appsv1.StatefulSet{}, deleteOnlyPred).
Owns(&corev1.ConfigMap{}, updateOrDeleteOnlyPred).
Owns(&corev1.Service{}, updateOrDeleteOnlyPred).
Owns(&appsv1.Deployment{}, updateOrDeleteOnlyPred).
Owns(&appsv1.StatefulSet{}, updateOrDeleteOnlyPred).
Complete(r)
}

@ -65,7 +65,7 @@ func TestLokiStackController_RegistersCustomResourceForCreateOrUpdate(t *testing
require.Equal(t, opts[0], createOrUpdateOnlyPred)
}
func TestLokiStackController_RegisterOwnedResourcesForDeleteOnly(t *testing.T) {
func TestLokiStackController_RegisterOwnedResourcesForUpdateOrDeleteOnly(t *testing.T) {
b := &k8sfakes.FakeBuilder{}
k := &k8sfakes.FakeClient{}
c := &LokiStackReconciler{Client: k, Scheme: scheme}
@ -87,19 +87,19 @@ func TestLokiStackController_RegisterOwnedResourcesForDeleteOnly(t *testing.T) {
table := []test{
{
obj: &corev1.ConfigMap{},
pred: deleteOnlyPred,
pred: updateOrDeleteOnlyPred,
},
{
obj: &corev1.Service{},
pred: deleteOnlyPred,
pred: updateOrDeleteOnlyPred,
},
{
obj: &appsv1.Deployment{},
pred: deleteOnlyPred,
pred: updateOrDeleteOnlyPred,
},
{
obj: &appsv1.StatefulSet{},
pred: deleteOnlyPred,
pred: updateOrDeleteOnlyPred,
},
}
for i, tst := range table {

@ -107,7 +107,7 @@ func NewCompactorStatefulSet(opt Options) *appsv1.StatefulSet {
podSpec.NodeSelector = opt.Stack.Template.Compactor.NodeSelector
}
l := ComponentLabels("compactor", opt.Name)
l := ComponentLabels(LabelCompactorComponent, opt.Name)
a := commonAnnotations(opt.ConfigSHA1)
return &appsv1.StatefulSet{
@ -116,7 +116,7 @@ func NewCompactorStatefulSet(opt Options) *appsv1.StatefulSet {
APIVersion: appsv1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("loki-compactor-%s", opt.Name),
Name: CompactorName(opt.Name),
Labels: l,
},
Spec: appsv1.StatefulSetSpec{

@ -121,7 +121,7 @@ func NewDistributorDeployment(opt Options) *appsv1.Deployment {
podSpec.NodeSelector = opt.Stack.Template.Distributor.NodeSelector
}
l := ComponentLabels("distributor", opt.Name)
l := ComponentLabels(LabelDistributorComponent, opt.Name)
a := commonAnnotations(opt.ConfigSHA1)
return &appsv1.Deployment{
@ -130,7 +130,7 @@ func NewDistributorDeployment(opt Options) *appsv1.Deployment {
APIVersion: appsv1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("loki-distributor-%s", opt.Name),
Name: DistributorName(opt.Name),
Labels: l,
},
Spec: appsv1.DeploymentSpec{

@ -110,7 +110,7 @@ func NewIngesterStatefulSet(opt Options) *appsv1.StatefulSet {
podSpec.NodeSelector = opt.Stack.Template.Ingester.NodeSelector
}
l := ComponentLabels("ingester", opt.Name)
l := ComponentLabels(LabelIngesterComponent, opt.Name)
a := commonAnnotations(opt.ConfigSHA1)
return &appsv1.StatefulSet{
@ -119,7 +119,7 @@ func NewIngesterStatefulSet(opt Options) *appsv1.StatefulSet {
APIVersion: appsv1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("loki-ingester-%s", opt.Name),
Name: IngesterName(opt.Name),
Labels: l,
},
Spec: appsv1.StatefulSetSpec{

@ -110,7 +110,7 @@ func NewQuerierStatefulSet(opt Options) *appsv1.StatefulSet {
podSpec.NodeSelector = opt.Stack.Template.Querier.NodeSelector
}
l := ComponentLabels("querier", opt.Name)
l := ComponentLabels(LabelQuerierComponent, opt.Name)
a := commonAnnotations(opt.ConfigSHA1)
return &appsv1.StatefulSet{
@ -119,7 +119,7 @@ func NewQuerierStatefulSet(opt Options) *appsv1.StatefulSet {
APIVersion: appsv1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("loki-querier-%s", opt.Name),
Name: QuerierName(opt.Name),
Labels: l,
},
Spec: appsv1.StatefulSetSpec{

@ -111,7 +111,7 @@ func NewQueryFrontendDeployment(opt Options) *appsv1.Deployment {
podSpec.NodeSelector = opt.Stack.Template.QueryFrontend.NodeSelector
}
l := ComponentLabels("query-frontend", opt.Name)
l := ComponentLabels(LabelQueryFrontendComponent, opt.Name)
a := commonAnnotations(opt.ConfigSHA1)
return &appsv1.Deployment{
@ -120,7 +120,7 @@ func NewQueryFrontendDeployment(opt Options) *appsv1.Deployment {
APIVersion: appsv1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("loki-query-frontend-%s", opt.Name),
Name: QueryFrontendName(opt.Name),
Labels: l,
},
Spec: appsv1.DeploymentSpec{

@ -15,6 +15,19 @@ const (
DefaultContainerImage = "docker.io/grafana/loki:2.2.1"
)
const (
// LabelCompactorComponent is the label value for the compactor component
LabelCompactorComponent string = "compactor"
// LabelDistributorComponent is the label value for the distributor component
LabelDistributorComponent string = "distributor"
// LabelIngesterComponent is the label value for the ingester component
LabelIngesterComponent string = "ingester"
// LabelQuerierComponent is the label value for the querier component
LabelQuerierComponent string = "querier"
// LabelQueryFrontendComponent is the label value for the query frontend component
LabelQueryFrontendComponent string = "query-frontend"
)
func commonAnnotations(h string) map[string]string {
return map[string]string{
"loki.openshift.io/config-hash": h,
@ -43,6 +56,31 @@ func GossipLabels() map[string]string {
}
}
// CompactorName is the name of the compactor statefulset
func CompactorName(stackName string) string {
return fmt.Sprintf("loki-compactor-%s", stackName)
}
// DistributorName is the name of the distibutor deployment
func DistributorName(stackName string) string {
return fmt.Sprintf("loki-distributor-%s", stackName)
}
// IngesterName is the name of the compactor statefulset
func IngesterName(stackName string) string {
return fmt.Sprintf("loki-ingester-%s", stackName)
}
// QuerierName is the name of the querier statefulset
func QuerierName(stackName string) string {
return fmt.Sprintf("loki-querier-%s", stackName)
}
// QueryFrontendName is the name of the query-frontend statefulset
func QueryFrontendName(stackName string) string {
return fmt.Sprintf("loki-query-frontend-%s", stackName)
}
func serviceNameQuerierHTTP(stackName string) string {
return fmt.Sprintf("loki-querier-http-%s", stackName)
}

Loading…
Cancel
Save