feat(edge/stacks): use namespace in manifest [EE-4507] (#8145)

pull/8197/head
Chaim Lev-Ari 2022-12-13 22:56:47 +02:00 committed by GitHub
parent 8936ae9b7a
commit 930d9e5628
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 112 additions and 47 deletions

View File

@ -92,6 +92,8 @@ type swarmStackFromFileContentPayload struct {
DeploymentType portainer.EdgeStackDeploymentType `example:"0" enums:"0,1,2"` DeploymentType portainer.EdgeStackDeploymentType `example:"0" enums:"0,1,2"`
// List of Registries to use for this stack // List of Registries to use for this stack
Registries []portainer.RegistryID Registries []portainer.RegistryID
// Uses the manifest's namespaces instead of the default one
UseManifestNamespaces bool
} }
func (payload *swarmStackFromFileContentPayload) Validate(r *http.Request) error { func (payload *swarmStackFromFileContentPayload) Validate(r *http.Request) error {
@ -114,7 +116,7 @@ func (handler *Handler) createSwarmStackFromFileContent(r *http.Request, dryrun
return nil, err return nil, err
} }
stack, err := handler.edgeStacksService.BuildEdgeStack(payload.Name, payload.DeploymentType, payload.EdgeGroups, payload.Registries) stack, err := handler.edgeStacksService.BuildEdgeStack(payload.Name, payload.DeploymentType, payload.EdgeGroups, payload.Registries, payload.UseManifestNamespaces)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to create Edge stack object") return nil, errors.Wrap(err, "failed to create Edge stack object")
} }
@ -197,6 +199,8 @@ type swarmStackFromGitRepositoryPayload struct {
DeploymentType portainer.EdgeStackDeploymentType `example:"0" enums:"0,1,2"` DeploymentType portainer.EdgeStackDeploymentType `example:"0" enums:"0,1,2"`
// List of Registries to use for this stack // List of Registries to use for this stack
Registries []portainer.RegistryID Registries []portainer.RegistryID
// Uses the manifest's namespaces instead of the default one
UseManifestNamespaces bool
} }
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error { func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
@ -230,7 +234,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request, dryru
return nil, err return nil, err
} }
stack, err := handler.edgeStacksService.BuildEdgeStack(payload.Name, payload.DeploymentType, payload.EdgeGroups, payload.Registries) stack, err := handler.edgeStacksService.BuildEdgeStack(payload.Name, payload.DeploymentType, payload.EdgeGroups, payload.Registries, payload.UseManifestNamespaces)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to create edge stack object") return nil, errors.Wrap(err, "failed to create edge stack object")
} }
@ -268,6 +272,8 @@ type swarmStackFromFileUploadPayload struct {
// nomad deploytype is enabled only for nomad environments(endpoints) // nomad deploytype is enabled only for nomad environments(endpoints)
DeploymentType portainer.EdgeStackDeploymentType `example:"0" enums:"0,1,2"` DeploymentType portainer.EdgeStackDeploymentType `example:"0" enums:"0,1,2"`
Registries []portainer.RegistryID Registries []portainer.RegistryID
// Uses the manifest's namespaces instead of the default one
UseManifestNamespaces bool
} }
func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error { func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error {
@ -303,6 +309,9 @@ func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error
} }
payload.Registries = registries payload.Registries = registries
useManifestNamespaces, _ := request.RetrieveBooleanMultiPartFormValue(r, "UseManifestNamespaces", true)
payload.UseManifestNamespaces = useManifestNamespaces
return nil return nil
} }
@ -313,7 +322,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(r *http.Request, dryrun b
return nil, err return nil, err
} }
stack, err := handler.edgeStacksService.BuildEdgeStack(payload.Name, payload.DeploymentType, payload.EdgeGroups, payload.Registries) stack, err := handler.edgeStacksService.BuildEdgeStack(payload.Name, payload.DeploymentType, payload.EdgeGroups, payload.Registries, payload.UseManifestNamespaces)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to create edge stack object") return nil, errors.Wrap(err, "failed to create edge stack object")
} }

View File

@ -19,6 +19,8 @@ type updateEdgeStackPayload struct {
Version *int Version *int
EdgeGroups []portainer.EdgeGroupID EdgeGroups []portainer.EdgeGroupID
DeploymentType portainer.EdgeStackDeploymentType DeploymentType portainer.EdgeStackDeploymentType
// Uses the manifest's namespaces instead of the default one
UseManifestNamespaces bool
} }
func (payload *updateEdgeStackPayload) Validate(r *http.Request) error { func (payload *updateEdgeStackPayload) Validate(r *http.Request) error {
@ -168,6 +170,8 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
stack.ManifestPath = filesystem.ManifestFileDefaultName stack.ManifestPath = filesystem.ManifestFileDefaultName
} }
stack.UseManifestNamespaces = payload.UseManifestNamespaces
hasDockerEndpoint, err := hasDockerEndpoint(handler.DataStore.Endpoint(), relatedEndpointIds) hasDockerEndpoint, err := hasDockerEndpoint(handler.DataStore.Endpoint(), relatedEndpointIds)
if err != nil { if err != nil {
return httperror.InternalServerError("Unable to check for existence of docker environment", err) return httperror.InternalServerError("Unable to check for existence of docker environment", err)

View File

@ -10,11 +10,14 @@ import (
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/middlewares" "github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/internal/endpointutils" "github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/kubernetes"
) )
type configResponse struct { type configResponse struct {
StackFileContent string StackFileContent string
Name string Name string
// Namespace to use for Kubernetes manifests, leave empty to use the namespaces defined in the manifest
Namespace string
} }
// @summary Inspect an Edge Stack for an Environment(Endpoint) // @summary Inspect an Edge Stack for an Environment(Endpoint)
@ -59,12 +62,18 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.
} }
} }
namespace := ""
if !edgeStack.UseManifestNamespaces {
namespace = kubernetes.DefaultNamespace
}
if endpointutils.IsKubernetesEndpoint(endpoint) { if endpointutils.IsKubernetesEndpoint(endpoint) {
fileName = edgeStack.ManifestPath fileName = edgeStack.ManifestPath
if fileName == "" { if fileName == "" {
return httperror.BadRequest("Kubernetes is not supported by this stack", errors.New("Kubernetes is not supported by this stack")) return httperror.BadRequest("Kubernetes is not supported by this stack", errors.New("Kubernetes is not supported by this stack"))
} }
} }
stackFileContent, err := handler.FileService.GetFileContent(edgeStack.ProjectPath, fileName) stackFileContent, err := handler.FileService.GetFileContent(edgeStack.ProjectPath, fileName)
@ -75,5 +84,6 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.
return response.JSON(w, configResponse{ return response.JSON(w, configResponse{
StackFileContent: string(stackFileContent), StackFileContent: string(stackFileContent),
Name: edgeStack.Name, Name: edgeStack.Name,
Namespace: namespace,
}) })
} }

View File

@ -30,7 +30,9 @@ func NewService(dataStore dataservices.DataStore) *Service {
func (service *Service) BuildEdgeStack(name string, func (service *Service) BuildEdgeStack(name string,
deploymentType portainer.EdgeStackDeploymentType, deploymentType portainer.EdgeStackDeploymentType,
edgeGroups []portainer.EdgeGroupID, edgeGroups []portainer.EdgeGroupID,
registries []portainer.RegistryID) (*portainer.EdgeStack, error) { registries []portainer.RegistryID,
useManifestNamespaces bool,
) (*portainer.EdgeStack, error) {
edgeStacksService := service.dataStore.EdgeStack() edgeStacksService := service.dataStore.EdgeStack()
err := validateUniqueName(edgeStacksService.EdgeStacks, name) err := validateUniqueName(edgeStacksService.EdgeStacks, name)
@ -40,13 +42,14 @@ func (service *Service) BuildEdgeStack(name string,
stackID := edgeStacksService.GetNextIdentifier() stackID := edgeStacksService.GetNextIdentifier()
return &portainer.EdgeStack{ return &portainer.EdgeStack{
ID: portainer.EdgeStackID(stackID), ID: portainer.EdgeStackID(stackID),
Name: name, Name: name,
DeploymentType: deploymentType, DeploymentType: deploymentType,
CreationDate: time.Now().Unix(), CreationDate: time.Now().Unix(),
EdgeGroups: edgeGroups, EdgeGroups: edgeGroups,
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus), Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
Version: 1, Version: 1,
UseManifestNamespaces: useManifestNamespaces,
}, nil }, nil
} }

View File

@ -0,0 +1,6 @@
package kubernetes
const (
// DefaultNamespace is the default namespace used when no namespace is specified
DefaultNamespace = "default"
)

View File

@ -281,6 +281,8 @@ type (
Version int `json:"Version"` Version int `json:"Version"`
ManifestPath string ManifestPath string
DeploymentType EdgeStackDeploymentType DeploymentType EdgeStackDeploymentType
// Uses the manifest's namespaces instead of the default one
UseManifestNamespaces bool
// Deprecated // Deprecated
Prune bool `json:"Prune"` Prune bool `json:"Prune"`

View File

@ -48,21 +48,33 @@
</editor-description> </editor-description>
</web-editor-form> </web-editor-form>
<web-editor-form <div ng-if="$ctrl.model.DeploymentType === 1">
ng-if="$ctrl.model.DeploymentType === 1" <div class="form-group">
value="$ctrl.model.StackFileContent" <div class="col-sm-12">
yml="true" <por-switch-field
identifier="kube-manifest-editor" label="'Use namespace(s) specified from manifest'"
placeholder="# Define or paste the content of your manifest here" tooltip="'If you have defined namespaces in your deployment file turning this on will enforce the use of those only in the deployment'"
on-change="($ctrl.onChangeKubeManifest)" checked="$ctrl.formValues.UseManifestNamespaces"
> on-change="($ctrl.onChangeUseManifestNamespaces)"
<editor-description> ></por-switch-field>
<p> </div>
You can get more information about Kubernetes file format in the </div>
<a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/" target="_blank">official documentation</a>.
</p> <web-editor-form
</editor-description> value="$ctrl.model.StackFileContent"
</web-editor-form> yml="true"
identifier="kube-manifest-editor"
placeholder="# Define or paste the content of your manifest here"
on-change="($ctrl.onChangeKubeManifest)"
>
<editor-description>
<p>
You can get more information about Kubernetes file format in the
<a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/" target="_blank">official documentation</a>.
</p>
</editor-description>
</web-editor-form>
</div>
<!-- actions --> <!-- actions -->
<div class="col-sm-12 form-section-title"> Actions </div> <div class="col-sm-12 form-section-title"> Actions </div>

View File

@ -22,6 +22,13 @@ export class EditEdgeStackFormController {
this.onChangeDeploymentType = this.onChangeDeploymentType.bind(this); this.onChangeDeploymentType = this.onChangeDeploymentType.bind(this);
this.removeLineBreaks = this.removeLineBreaks.bind(this); this.removeLineBreaks = this.removeLineBreaks.bind(this);
this.onChangeFileContent = this.onChangeFileContent.bind(this); this.onChangeFileContent = this.onChangeFileContent.bind(this);
this.onChangeUseManifestNamespaces = this.onChangeUseManifestNamespaces.bind(this);
}
onChangeUseManifestNamespaces(value) {
this.$scope.$evalAsync(() => {
this.model.UseManifestNamespaces = value;
});
} }
hasKubeEndpoint() { hasKubeEndpoint() {

View File

@ -16,6 +16,7 @@ export default class CreateEdgeStackViewController {
ComposeFilePathInRepository: '', ComposeFilePathInRepository: '',
Groups: [], Groups: [],
DeploymentType: 0, DeploymentType: 0,
UseManifestNamespaces: false,
}; };
this.state = { this.state = {
@ -166,30 +167,32 @@ export default class CreateEdgeStackViewController {
} }
createStackFromFileContent(name) { createStackFromFileContent(name) {
const { StackFileContent, Groups, DeploymentType } = this.formValues; const { StackFileContent, Groups, DeploymentType, UseManifestNamespaces } = this.formValues;
return this.EdgeStackService.createStackFromFileContent({ return this.EdgeStackService.createStackFromFileContent({
name, name,
StackFileContent, StackFileContent,
EdgeGroups: Groups, EdgeGroups: Groups,
DeploymentType, DeploymentType,
UseManifestNamespaces,
}); });
} }
createStackFromFileUpload(name) { createStackFromFileUpload(name) {
const { StackFile, Groups, DeploymentType } = this.formValues; const { StackFile, Groups, DeploymentType, UseManifestNamespaces } = this.formValues;
return this.EdgeStackService.createStackFromFileUpload( return this.EdgeStackService.createStackFromFileUpload(
{ {
Name: name, Name: name,
EdgeGroups: Groups, EdgeGroups: Groups,
DeploymentType, DeploymentType,
UseManifestNamespaces,
}, },
StackFile StackFile
); );
} }
createStackFromGitRepository(name) { createStackFromGitRepository(name) {
const { Groups, DeploymentType } = this.formValues; const { Groups, DeploymentType, UseManifestNamespaces } = this.formValues;
const repositoryOptions = { const repositoryOptions = {
RepositoryURL: this.formValues.RepositoryURL, RepositoryURL: this.formValues.RepositoryURL,
RepositoryReferenceName: this.formValues.RepositoryReferenceName, RepositoryReferenceName: this.formValues.RepositoryReferenceName,
@ -203,6 +206,7 @@ export default class CreateEdgeStackViewController {
name, name,
EdgeGroups: Groups, EdgeGroups: Groups,
DeploymentType, DeploymentType,
UseManifestNamespaces,
}, },
repositoryOptions repositoryOptions
); );

View File

@ -11,10 +11,20 @@ class KubeManifestFormController {
this.onChangeFormValues = this.onChangeFormValues.bind(this); this.onChangeFormValues = this.onChangeFormValues.bind(this);
this.onChangeFile = this.onChangeFile.bind(this); this.onChangeFile = this.onChangeFile.bind(this);
this.onChangeMethod = this.onChangeMethod.bind(this); this.onChangeMethod = this.onChangeMethod.bind(this);
this.onChangeUseManifestNamespaces = this.onChangeUseManifestNamespaces.bind(this);
} }
onChangeFormValues(values) { onChangeFormValues(newValues) {
this.formValues = values; return this.$async(async () => {
this.formValues = {
...this.formValues,
...newValues,
};
});
}
onChangeUseManifestNamespaces(value) {
this.onChangeFormValues({ UseManifestNamespaces: value });
} }
onChangeFileContent(value) { onChangeFileContent(value) {

View File

@ -1,3 +1,14 @@
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
label="'Use namespace(s) specified from manifest'"
tooltip="'If you have defined namespaces in your deployment file turning this on will enforce the use of those only in the deployment'"
checked="$ctrl.formValues.UseManifestNamespaces"
on-change="($ctrl.onChangeUseManifestNamespaces)"
></por-switch-field>
</div>
</div>
<div class="col-sm-12 form-section-title"> Build method </div> <div class="col-sm-12 form-section-title"> Build method </div>
<box-selector radio-name="'method'" value="$ctrl.state.Method" options="$ctrl.methodOptions" on-change="($ctrl.onChangeMethod)"></box-selector> <box-selector radio-name="'method'" value="$ctrl.state.Method" options="$ctrl.methodOptions" on-change="($ctrl.onChangeMethod)"></box-selector>

View File

@ -39,6 +39,7 @@ export class EditEdgeStackViewController {
this.formValues = { this.formValues = {
StackFileContent: file, StackFileContent: file,
EdgeGroups: this.stack.EdgeGroups, EdgeGroups: this.stack.EdgeGroups,
UseManifestNamespaces: this.stack.UseManifestNamespaces,
DeploymentType: this.stack.DeploymentType, DeploymentType: this.stack.DeploymentType,
}; };
this.oldFileContent = this.formValues.StackFileContent; this.oldFileContent = this.formValues.StackFileContent;
@ -79,7 +80,7 @@ export class EditEdgeStackViewController {
async deployStackAsync() { async deployStackAsync() {
this.state.actionInProgress = true; this.state.actionInProgress = true;
try { try {
if (this.originalFileContent != this.formValues.StackFileContent) { if (this.originalFileContent != this.formValues.StackFileContent || this.formValues.UseManifestNamespaces !== this.stack.UseManifestNamespaces) {
this.formValues.Version = this.stack.Version + 1; this.formValues.Version = this.stack.Version + 1;
} }
await this.EdgeStackService.updateStack(this.stack.Id, this.formValues); await this.EdgeStackService.updateStack(this.stack.Id, this.formValues);

View File

@ -3,13 +3,3 @@
align-items: center; align-items: center;
margin: 0; margin: 0;
} }
.label {
padding: 0;
}
.switchValues {
font-style: normal;
font-weight: 500;
margin-left: 5px;
}

View File

@ -42,11 +42,7 @@ export function SwitchField({
return ( return (
<label className={clsx(styles.root, fieldClass)}> <label className={clsx(styles.root, fieldClass)}>
<span <span
className={clsx( className={clsx('text-left space-right control-label !p-0', labelClass)}
'text-left space-right control-label',
styles.label,
labelClass
)}
> >
{label} {label}
{tooltip && ( {tooltip && (