feat(k8s): advanced deployment from Git repo EE-447 (#5166)

* feat(stack): UI updates in git repo deployment method for k8s EE-640. (#5097)

* feat(stack): UI updates in git repo deployment method for k8s EE-640.

* feat(stack): supports the combination of GIT + COMPOSE.

* feat(stack): rename variable

* feat(stack): add git repo deployment method for k8s EE-638

* cleanup

* update payload validation rules

* make repo ref optional in frond end

Co-authored-by: fhanportainer <79428273+fhanportainer@users.noreply.github.com>
pull/5203/head
Hui 2021-06-17 09:47:32 +12:00 committed by GitHub
parent 6b759438b8
commit caa6c15032
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 409 additions and 63 deletions

View File

@ -31,6 +31,8 @@ const (
ComposeStorePath = "compose" ComposeStorePath = "compose"
// ComposeFileDefaultName represents the default name of a compose file. // ComposeFileDefaultName represents the default name of a compose file.
ComposeFileDefaultName = "docker-compose.yml" ComposeFileDefaultName = "docker-compose.yml"
// ManifestFileDefaultName represents the default name of a k8s manifest file.
ManifestFileDefaultName = "k8s-deployment.yml"
// EdgeStackStorePath represents the subfolder where edge stack files are stored in the file store folder. // EdgeStackStorePath represents the subfolder where edge stack files are stored in the file store folder.
EdgeStackStorePath = "edge_stacks" EdgeStackStorePath = "edge_stacks"
// PrivateKeyFile represents the name on disk of the file containing the private key. // PrivateKeyFile represents the name on disk of the file containing the private key.

View File

@ -237,11 +237,11 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false) isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false)
if err != nil { if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err} return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
} }
if !isUnique { if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' already exists", payload.Name) errorMessage := fmt.Sprintf("A stack with the name '%s' already exists", payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)} return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: errorMessage, Err: errors.New(errorMessage)}
} }
stackID := handler.DataStore.Stack().GetNextIdentifier() stackID := handler.DataStore.Stack().GetNextIdentifier()

View File

@ -2,7 +2,11 @@ package stacks
import ( import (
"errors" "errors"
"io/ioutil"
"net/http" "net/http"
"path/filepath"
"strconv"
"time"
"github.com/asaskevich/govalidator" "github.com/asaskevich/govalidator"
@ -10,16 +14,29 @@ import (
"github.com/portainer/libhttp/request" "github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response" "github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
endpointutils "github.com/portainer/portainer/api/internal/endpoint" "github.com/portainer/portainer/api/filesystem"
) )
type kubernetesStackPayload struct { const defaultReferenceName = "refs/heads/master"
type kubernetesStringDeploymentPayload struct {
ComposeFormat bool ComposeFormat bool
Namespace string Namespace string
StackFileContent string StackFileContent string
} }
func (payload *kubernetesStackPayload) Validate(r *http.Request) error { type kubernetesGitDeploymentPayload struct {
ComposeFormat bool
Namespace string
RepositoryURL string
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
FilePathInRepository string
}
func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.StackFileContent) { if govalidator.IsNull(payload.StackFileContent) {
return errors.New("Invalid stack file content") return errors.New("Invalid stack file content")
} }
@ -29,24 +46,63 @@ func (payload *kubernetesStackPayload) Validate(r *http.Request) error {
return nil return nil
} }
func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Namespace) {
return errors.New("Invalid namespace")
}
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
}
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
}
if govalidator.IsNull(payload.FilePathInRepository) {
return errors.New("Invalid file path in repository")
}
if govalidator.IsNull(payload.RepositoryReferenceName) {
payload.RepositoryReferenceName = defaultReferenceName
}
return nil
}
type createKubernetesStackResponse struct { type createKubernetesStackResponse struct {
Output string `json:"Output"` Output string `json:"Output"`
} }
func (handler *Handler) createKubernetesStack(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError { func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
if !endpointutils.IsKubernetesEndpoint(endpoint) { var payload kubernetesStringDeploymentPayload
return &httperror.HandlerError{http.StatusBadRequest, "Endpoint type does not match", errors.New("Endpoint type does not match")} if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
} }
var payload kubernetesStackPayload stackID := handler.DataStore.Stack().GetNextIdentifier()
err := request.DecodeAndValidateJSONPayload(r, &payload) stack := &portainer.Stack{
if err != nil { ID: portainer.StackID(stackID),
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} Type: portainer.KubernetesStack,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ManifestFileDefaultName,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
} }
stackFolder := strconv.Itoa(int(stack.ID))
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist Kubernetes manifest file on disk", Err: err}
}
stack.ProjectPath = projectPath
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
output, err := handler.deployKubernetesStack(endpoint, payload.StackFileContent, payload.ComposeFormat, payload.Namespace) output, err := handler.deployKubernetesStack(endpoint, payload.StackFileContent, payload.ComposeFormat, payload.Namespace)
if err != nil { if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to deploy Kubernetes stack", err} return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
}
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the Kubernetes stack inside the database", Err: err}
} }
resp := &createKubernetesStackResponse{ resp := &createKubernetesStackResponse{
@ -56,6 +112,49 @@ func (handler *Handler) createKubernetesStack(w http.ResponseWriter, r *http.Req
return response.JSON(w, resp) return response.JSON(w, resp)
} }
func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
var payload kubernetesGitDeploymentPayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Type: portainer.KubernetesStack,
EndpointID: endpoint.ID,
EntryPoint: payload.FilePathInRepository,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
}
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
stack.ProjectPath = projectPath
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
stackFileContent, err := handler.cloneManifestContentFromGitRepo(&payload, stack.ProjectPath)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to process manifest from Git repository", Err: err}
}
output, err := handler.deployKubernetesStack(endpoint, stackFileContent, payload.ComposeFormat, payload.Namespace)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
}
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack inside the database", Err: err}
}
resp := &createKubernetesStackResponse{
Output: output,
}
return response.JSON(w, resp)
}
func (handler *Handler) deployKubernetesStack(endpoint *portainer.Endpoint, stackConfig string, composeFormat bool, namespace string) (string, error) { func (handler *Handler) deployKubernetesStack(endpoint *portainer.Endpoint, stackConfig string, composeFormat bool, namespace string) (string, error) {
handler.stackCreationMutex.Lock() handler.stackCreationMutex.Lock()
defer handler.stackCreationMutex.Unlock() defer handler.stackCreationMutex.Unlock()
@ -71,3 +170,15 @@ func (handler *Handler) deployKubernetesStack(endpoint *portainer.Endpoint, stac
return handler.KubernetesDeployer.Deploy(endpoint, stackConfig, namespace) return handler.KubernetesDeployer.Deploy(endpoint, stackConfig, namespace)
} }
func (handler *Handler) cloneManifestContentFromGitRepo(gitInfo *kubernetesGitDeploymentPayload, projectPath string) (string, error) {
err := handler.GitService.CloneRepository(projectPath, gitInfo.RepositoryURL, gitInfo.RepositoryReferenceName, gitInfo.RepositoryUsername, gitInfo.RepositoryPassword)
if err != nil {
return "", err
}
content, err := ioutil.ReadFile(filepath.Join(projectPath, gitInfo.FilePathInRepository))
if err != nil {
return "", err
}
return string(content), nil
}

View File

@ -0,0 +1,64 @@
package stacks
import (
"io/ioutil"
"os"
"path"
"testing"
"github.com/stretchr/testify/assert"
)
type git struct {
content string
}
func (g *git) CloneRepository(destination string, repositoryURL, referenceName, username, password string) error {
return g.ClonePublicRepository(repositoryURL, referenceName, destination)
}
func (g *git) ClonePublicRepository(repositoryURL string, referenceName string, destination string) error {
return ioutil.WriteFile(path.Join(destination, "deployment.yml"), []byte(g.content), 0755)
}
func (g *git) ClonePrivateRepositoryWithBasicAuth(repositoryURL, referenceName string, destination, username, password string) error {
return g.ClonePublicRepository(repositoryURL, referenceName, destination)
}
func TestCloneAndConvertGitRepoFile(t *testing.T) {
dir, err := os.MkdirTemp("", "kube-create-stack")
assert.NoError(t, err, "failed to create a tmp dir")
defer os.RemoveAll(dir)
content := `apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80`
h := &Handler{
GitService: &git{
content: content,
},
}
gitInfo := &kubernetesGitDeploymentPayload{
FilePathInRepository: "deployment.yml",
}
fileContent, err := h.cloneManifestContentFromGitRepo(gitInfo, dir)
assert.NoError(t, err, "failed to clone or convert the file from Git repo")
assert.Equal(t, content, fileContent)
}

View File

@ -14,6 +14,7 @@ import (
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors" bolterrors "github.com/portainer/portainer/api/bolt/errors"
gittypes "github.com/portainer/portainer/api/git/types" gittypes "github.com/portainer/portainer/api/git/types"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization" "github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/endpointutils" "github.com/portainer/portainer/api/internal/endpointutils"
@ -112,7 +113,11 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
case portainer.DockerComposeStack: case portainer.DockerComposeStack:
return handler.createComposeStack(w, r, method, endpoint, tokenData.ID) return handler.createComposeStack(w, r, method, endpoint, tokenData.ID)
case portainer.KubernetesStack: case portainer.KubernetesStack:
return handler.createKubernetesStack(w, r, endpoint) if tokenData.Role != portainer.AdministratorRole {
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied", Err: httperrors.ErrUnauthorized}
}
return handler.createKubernetesStack(w, r, method, endpoint)
} }
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: type. Value must be one of: 1 (Swarm stack) or 2 (Compose stack)", errors.New(request.ErrInvalidQueryParameter)} return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: type. Value must be one of: 1 (Swarm stack) or 2 (Compose stack)", errors.New(request.ErrInvalidQueryParameter)}
@ -145,6 +150,16 @@ func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request,
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)} return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)}
} }
func (handler *Handler) createKubernetesStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint) *httperror.HandlerError {
switch method {
case "string":
return handler.createKubernetesStackFromFileContent(w, r, endpoint)
case "repository":
return handler.createKubernetesStackFromGitRepository(w, r, endpoint)
}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid value for query parameter: method. Value must be one of: string or repository", Err: errors.New(request.ErrInvalidQueryParameter)}
}
func (handler *Handler) isValidStackFile(stackFileContent []byte, securitySettings *portainer.EndpointSecuritySettings) error { func (handler *Handler) isValidStackFile(stackFileContent []byte, securitySettings *portainer.EndpointSecuritySettings) error {
composeConfigYAML, err := loader.ParseYAML(stackFileContent) composeConfigYAML, err := loader.ParseYAML(stackFileContent)
if err != nil { if err != nil {

View File

@ -2,3 +2,13 @@ export const KubernetesDeployManifestTypes = Object.freeze({
KUBERNETES: 1, KUBERNETES: 1,
COMPOSE: 2, COMPOSE: 2,
}); });
export const KubernetesDeployBuildMethods = Object.freeze({
GIT: 1,
WEB_EDITOR: 2,
});
export const KubernetesDeployRequestMethods = Object.freeze({
REPOSITORY: 'repository',
STRING: 'string',
});

View File

@ -207,8 +207,8 @@
<ng-messages for="kubernetesApplicationCreationForm['environment_variable_name_' + $index].$error"> <ng-messages for="kubernetesApplicationCreationForm['environment_variable_name_' + $index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Environment variable name is required.</p> <p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Environment variable name is required.</p>
<p ng-message="pattern" <p ng-message="pattern"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist of alphabetic characters, digits, '_', '-', or '.', and must ><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist of alphabetic characters, digits, '_', '-', or '.', and must not
not start with a digit (e.g. 'my.env-name', or 'MY_ENV.NAME', or 'MyEnvName1'.</p start with a digit (e.g. 'my.env-name', or 'MY_ENV.NAME', or 'MyEnvName1'.</p
> >
</ng-messages> </ng-messages>
<p ng-if="ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined" <p ng-if="ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined"

View File

@ -52,7 +52,122 @@
</div> </div>
</div> </div>
<!-- !deploy-type --> <!-- !deploy-type -->
<!-- build method -->
<div class="col-sm-12 form-section-title">
Build method
</div>
<div class="form-group"></div>
<div class="form-group" style="margin-bottom: 0;">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="build_method_git" ng-model="ctrl.state.BuildMethod" ng-value="ctrl.BuildMethods.GIT" />
<label for="build_method_git">
<div class="boxselector_header">
<i class="fab fa-github" aria-hidden="true" style="margin-right: 2px;"></i>
Git Repository
</div>
<p>Use a git repository</p>
</label>
</div>
<div>
<input type="radio" id="build_method_web_editor" ng-model="ctrl.state.BuildMethod" ng-value="ctrl.BuildMethods.WEB_EDITOR" />
<label for="build_method_web_editor">
<div class="boxselector_header">
<i class="fa fa-edit" aria-hidden="true" style="margin-right: 2px;"></i>
Web editor
</div>
<p>Use our Web editor</p>
</label>
</div>
</div>
</div>
<!-- !deploy-type -->
<!-- repository -->
<div ng-show="ctrl.state.BuildMethod === ctrl.BuildMethods.GIT">
<div class="col-sm-12 form-section-title">
Git repository
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
You can use the URL of a git repository.
</span>
</div>
<div class="form-group">
<label for="stack_repository_url" class="col-sm-2 control-label text-left">Repository URL</label>
<div class="col-sm-10">
<input
type="text"
class="form-control"
ng-model="ctrl.formValues.RepositoryURL"
id="stack_repository_url"
placeholder="https://github.com/portainer/deployment-repository"
/>
</div>
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
Specify a reference of the repository using the following syntax: branches with
<code>refs/heads/branch_name</code> or tags with <code>refs/tags/tag_name</code>. If not specified, will use the default <code>HEAD</code> reference normally
the <code>master</code> branch.
</span>
</div>
<div class="form-group">
<label for="stack_repository_url" class="col-sm-2 control-label text-left">Repository reference</label>
<div class="col-sm-10">
<input
type="text"
class="form-control"
ng-model="ctrl.formValues.RepositoryReferenceName"
id="stack_repository_reference_name"
placeholder="refs/heads/master"
/>
</div>
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
Indicate the path to the yaml file from the root of your repository.
</span>
</div>
<div class="form-group">
<label for="stack_repository_path" class="col-sm-2 control-label text-left">Manifest path</label>
<div class="col-sm-10">
<input type="text" class="form-control" ng-model="ctrl.formValues.FilePathInRepository" id="stack_manifest_path" placeholder="deployment.yml" />
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Authentication
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.RepositoryAuthentication" /><i></i> </label>
</div>
</div>
<div class="form-group" ng-if="ctrl.formValues.RepositoryAuthentication">
<span class="col-sm-12 text-muted small">
If your git account has 2FA enabled, you may receive an
<code>authentication required</code> error when deploying your stack. In this case, you will need to provide a personal-access token instead of your password.
</span>
</div>
<div class="form-group" ng-if="ctrl.formValues.RepositoryAuthentication">
<label for="repository_username" class="col-sm-1 control-label text-left">Username</label>
<div class="col-sm-11 col-md-5">
<input type="text" class="form-control" ng-model="ctrl.formValues.RepositoryUsername" name="repository_username" placeholder="myGitUser" />
</div>
<label for="repository_password" class="col-sm-1 control-label text-left">
Password
</label>
<div class="col-sm-11 col-md-5">
<input type="password" class="form-control" ng-model="ctrl.formValues.RepositoryPassword" name="repository_password" placeholder="myPassword" />
</div>
</div>
</div>
<!-- !repository -->
<!-- editor --> <!-- editor -->
<div ng-if="ctrl.state.BuildMethod === ctrl.BuildMethods.WEB_EDITOR">
<div class="col-sm-12 form-section-title"> <div class="col-sm-12 form-section-title">
Web editor Web editor
</div> </div>
@ -60,15 +175,18 @@
<span class="col-sm-12 text-muted small" ng-show="ctrl.state.DeployType === ctrl.ManifestDeployTypes.COMPOSE"> <span class="col-sm-12 text-muted small" ng-show="ctrl.state.DeployType === ctrl.ManifestDeployTypes.COMPOSE">
<p> <p>
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i> <i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Portainer uses <a href="https://kompose.io/" target="_blank">Kompose</a> to convert your Compose manifest to a Kubernetes compliant manifest. Be wary that not Portainer uses <a href="https://kompose.io/" target="_blank">Kompose</a> to convert your Compose manifest to a Kubernetes compliant manifest. Be wary that
all the Compose format options are supported by Kompose at the moment. not all the Compose format options are supported by Kompose at the moment.
</p> </p>
<p> <p>
You can get more information about Compose file format in the You can get more information about Compose file format in the
<a href="https://docs.docker.com/compose/compose-file/" target="_blank">official documentation</a>. <a href="https://docs.docker.com/compose/compose-file/" target="_blank">official documentation</a>.
</p> </p>
</span> </span>
<span class="col-sm-12 text-muted small" ng-show="ctrl.state.DeployType === ctrl.ManifestDeployTypes.KUBERNETES"> <span
class="col-sm-12 text-muted small"
ng-show="ctrl.state.DeployType === ctrl.ManifestDeployTypes.KUBERNETES && ctrl.state.BuildMethod === ctrl.BuildMethods.WEB_EDITOR"
>
<p> <p>
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i> <i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This feature allows you to deploy any kind of Kubernetes resource in this environment (Deployment, Secret, ConfigMap...). This feature allows you to deploy any kind of Kubernetes resource in this environment (Deployment, Secret, ConfigMap...).
@ -79,16 +197,19 @@
</p> </p>
</span> </span>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-sm-12"> <div class="col-sm-12">
<code-editor <code-editor
identifier="kubernetes-deploy-editor" identifier="kubernetes-deploy-editor"
placeholder="# Define or paste the content of your manifest file here" placeholder="# Define or paste the content of your manifest file here"
yml="false" yml="false"
value="ctrl.formValues.EditorContent"
on-change="(ctrl.editorUpdate)" on-change="(ctrl.editorUpdate)"
></code-editor> ></code-editor>
</div> </div>
</div> </div>
</div>
<!-- !editor --> <!-- !editor -->
<!-- actions --> <!-- actions -->
<div class="col-sm-12 form-section-title"> <div class="col-sm-12 form-section-title">

View File

@ -1,7 +1,7 @@
import angular from 'angular'; import angular from 'angular';
import _ from 'lodash-es'; import _ from 'lodash-es';
import stripAnsi from 'strip-ansi'; import stripAnsi from 'strip-ansi';
import { KubernetesDeployManifestTypes } from 'Kubernetes/models/deploy'; import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, KubernetesDeployRequestMethods } from 'Kubernetes/models/deploy';
class KubernetesDeployController { class KubernetesDeployController {
/* @ngInject */ /* @ngInject */
@ -23,7 +23,14 @@ class KubernetesDeployController {
} }
disableDeploy() { disableDeploy() {
return _.isEmpty(this.formValues.EditorContent) || _.isEmpty(this.formValues.Namespace) || this.state.actionInProgress; const isGitFormInvalid =
this.state.BuildMethod === KubernetesDeployBuildMethods.GIT &&
(!this.formValues.RepositoryURL ||
!this.formValues.FilePathInRepository ||
(this.formValues.RepositoryAuthentication && (!this.formValues.RepositoryUsername || !this.formValues.RepositoryPassword)));
const isWebEditorInvalid = this.state.BuildMethod === KubernetesDeployBuildMethods.WEB_EDITOR && _.isEmpty(this.formValues.EditorContent);
return isGitFormInvalid || isWebEditorInvalid || _.isEmpty(this.formValues.Namespace) || this.state.actionInProgress;
} }
async editorUpdateAsync(cm) { async editorUpdateAsync(cm) {
@ -46,8 +53,28 @@ class KubernetesDeployController {
this.state.actionInProgress = true; this.state.actionInProgress = true;
try { try {
const compose = this.state.DeployType === this.ManifestDeployTypes.COMPOSE; const method = this.state.BuildMethod === this.BuildMethods.GIT ? KubernetesDeployRequestMethods.REPOSITORY : KubernetesDeployRequestMethods.STRING;
await this.StackService.kubernetesDeploy(this.endpointId, this.formValues.Namespace, this.formValues.EditorContent, compose);
const payload = {
ComposeFormat: this.state.DeployType === this.ManifestDeployTypes.COMPOSE,
Namespace: this.formValues.Namespace,
};
if (method === KubernetesDeployRequestMethods.REPOSITORY) {
payload.RepositoryURL = this.formValues.RepositoryURL;
payload.RepositoryReferenceName = this.formValues.RepositoryReferenceName;
payload.RepositoryAuthentication = this.formValues.RepositoryAuthentication ? true : false;
if (payload.RepositoryAuthentication) {
payload.RepositoryUsername = this.formValues.RepositoryUsername;
payload.RepositoryPassword = this.formValues.RepositoryPassword;
}
payload.FilePathInRepository = this.formValues.FilePathInRepository;
} else {
payload.StackFileContent = this.formValues.EditorContent;
}
await this.StackService.kubernetesDeploy(this.endpointId, method, payload);
this.Notifications.success('Manifest successfully deployed'); this.Notifications.success('Manifest successfully deployed');
this.state.isEditorDirty = false; this.state.isEditorDirty = false;
this.$state.go('kubernetes.applications'); this.$state.go('kubernetes.applications');
@ -92,10 +119,10 @@ class KubernetesDeployController {
return this.ModalService.confirmWebEditorDiscard(); return this.ModalService.confirmWebEditorDiscard();
} }
} }
async onInit() { async onInit() {
this.state = { this.state = {
DeployType: KubernetesDeployManifestTypes.KUBERNETES, DeployType: KubernetesDeployManifestTypes.KUBERNETES,
BuildMethod: KubernetesDeployBuildMethods.GIT,
tabLogsDisabled: true, tabLogsDisabled: true,
activeTab: 0, activeTab: 0,
viewReady: false, viewReady: false,
@ -104,6 +131,7 @@ class KubernetesDeployController {
this.formValues = {}; this.formValues = {};
this.ManifestDeployTypes = KubernetesDeployManifestTypes; this.ManifestDeployTypes = KubernetesDeployManifestTypes;
this.BuildMethods = KubernetesDeployBuildMethods;
this.endpointId = this.EndpointProvider.endpointID(); this.endpointId = this.EndpointProvider.endpointID();
await this.getNamespaces(); await this.getNamespaces();

View File

@ -369,21 +369,16 @@ angular.module('portainer.app').factory('StackService', [
return action(name, stackFileContent, env, endpointId); return action(name, stackFileContent, env, endpointId);
}; };
async function kubernetesDeployAsync(endpointId, namespace, content, compose) { async function kubernetesDeployAsync(endpointId, method, payload) {
try { try {
const payload = { await Stack.create({ endpointId: endpointId, method: method, type: 3 }, payload).$promise;
StackFileContent: content,
ComposeFormat: compose,
Namespace: namespace,
};
await Stack.create({ method: 'undefined', type: 3, endpointId: endpointId }, payload).$promise;
} catch (err) { } catch (err) {
throw { err: err }; throw { err: err };
} }
} }
service.kubernetesDeploy = function (endpointId, namespace, content, compose) { service.kubernetesDeploy = function (endpointId, method, payload) {
return $async(kubernetesDeployAsync, endpointId, namespace, content, compose); return $async(kubernetesDeployAsync, endpointId, method, payload);
}; };
service.start = start; service.start = start;