1105 lines
33 KiB
Go
1105 lines
33 KiB
Go
package customtemplates
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
|
|
portainer "github.com/portainer/portainer/api"
|
|
"github.com/portainer/portainer/api/dataservices"
|
|
"github.com/portainer/portainer/api/filesystem"
|
|
gittypes "github.com/portainer/portainer/api/git/types"
|
|
"github.com/portainer/portainer/api/http/security"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/segmentio/encoding/json"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func createTemplateRequest(t *testing.T, method string, payload any, userID portainer.UserID, role portainer.UserRole) *http.Request {
|
|
t.Helper()
|
|
|
|
body, err := json.Marshal(payload)
|
|
require.NoError(t, err)
|
|
|
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/"+method, bytes.NewReader(body))
|
|
r.Header.Set("Content-Type", "application/json")
|
|
r = mux.SetURLVars(r, map[string]string{"method": method})
|
|
|
|
return r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: userID, Role: role}))
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileContent_Success(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, ds, _ := newTestHandler(t)
|
|
|
|
payload := customTemplateFromFileContentPayload{
|
|
Title: "My Template",
|
|
Description: "A test template",
|
|
FileContent: "version: '3'\nservices:\n web:\n image: nginx",
|
|
Type: portainer.DockerComposeStack,
|
|
Platform: portainer.CustomTemplatePlatformLinux,
|
|
}
|
|
|
|
r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.Nil(t, herr)
|
|
require.Equal(t, http.StatusOK, rr.Code)
|
|
|
|
var tmpl portainer.CustomTemplate
|
|
require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl))
|
|
require.Equal(t, "My Template", tmpl.Title)
|
|
require.Equal(t, "A test template", tmpl.Description)
|
|
require.Equal(t, portainer.UserID(1), tmpl.CreatedByUserID)
|
|
require.NotNil(t, tmpl.ResourceControl)
|
|
|
|
err := ds.ViewTx(func(tx dataservices.DataStoreTx) error {
|
|
templates, err := tx.CustomTemplate().ReadAll()
|
|
require.NoError(t, err)
|
|
require.Len(t, templates, 1)
|
|
require.Equal(t, "My Template", templates[0].Title)
|
|
|
|
rcs, err := tx.ResourceControl().ReadAll()
|
|
require.NoError(t, err)
|
|
require.Len(t, rcs, 1)
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileContent_DuplicateTitle(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, ds, _ := newTestHandler(t)
|
|
|
|
require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
|
return tx.CustomTemplate().Create(&portainer.CustomTemplate{
|
|
ID: 1,
|
|
Title: "Existing Template",
|
|
})
|
|
}))
|
|
|
|
payload := customTemplateFromFileContentPayload{
|
|
Title: "Existing Template",
|
|
Description: "Another template",
|
|
FileContent: "version: '3'",
|
|
Type: portainer.DockerComposeStack,
|
|
Platform: portainer.CustomTemplatePlatformLinux,
|
|
}
|
|
|
|
r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileContent_MissingTitle(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
payload := customTemplateFromFileContentPayload{
|
|
Description: "A test template",
|
|
FileContent: "version: '3'",
|
|
Type: portainer.DockerComposeStack,
|
|
Platform: portainer.CustomTemplatePlatformLinux,
|
|
}
|
|
|
|
r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileContent_MissingDescription(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
payload := customTemplateFromFileContentPayload{
|
|
Title: "My Template",
|
|
FileContent: "version: '3'",
|
|
Type: portainer.DockerComposeStack,
|
|
Platform: portainer.CustomTemplatePlatformLinux,
|
|
}
|
|
|
|
r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileContent_MissingFileContent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
payload := customTemplateFromFileContentPayload{
|
|
Title: "My Template",
|
|
Description: "A test template",
|
|
Type: portainer.DockerComposeStack,
|
|
Platform: portainer.CustomTemplatePlatformLinux,
|
|
}
|
|
|
|
r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileContent_InvalidPlatform(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
payload := customTemplateFromFileContentPayload{
|
|
Title: "My Template",
|
|
Description: "A test template",
|
|
FileContent: "version: '3'",
|
|
Type: portainer.DockerComposeStack,
|
|
Platform: 0,
|
|
}
|
|
|
|
r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileContent_NoteWithImage(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
payload := customTemplateFromFileContentPayload{
|
|
Title: "My Template",
|
|
Description: "A test template",
|
|
FileContent: "version: '3'",
|
|
Note: `Some note with <img src="x" onerror="alert(1)">`,
|
|
Type: portainer.DockerComposeStack,
|
|
Platform: portainer.CustomTemplatePlatformLinux,
|
|
}
|
|
|
|
r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileContent_KubernetesTypeIgnoresPlatform(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, ds, _ := newTestHandler(t)
|
|
|
|
payload := customTemplateFromFileContentPayload{
|
|
Title: "K8s Template",
|
|
Description: "A kubernetes template",
|
|
FileContent: "apiVersion: v1\nkind: Pod",
|
|
Type: portainer.KubernetesStack,
|
|
Platform: 0,
|
|
}
|
|
|
|
r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.Nil(t, herr)
|
|
require.Equal(t, http.StatusOK, rr.Code)
|
|
|
|
var tmpl portainer.CustomTemplate
|
|
require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl))
|
|
require.Equal(t, "K8s Template", tmpl.Title)
|
|
require.Equal(t, portainer.KubernetesStack, tmpl.Type)
|
|
|
|
err := ds.ViewTx(func(tx dataservices.DataStoreTx) error {
|
|
templates, err := tx.CustomTemplate().ReadAll()
|
|
require.NoError(t, err)
|
|
require.Len(t, templates, 1)
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileUpload_Success(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, ds, _ := newTestHandler(t)
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
|
|
err := writer.WriteField("Title", "Uploaded Template")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Description", "Uploaded from file")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Type", "2")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Platform", "1")
|
|
require.NoError(t, err)
|
|
|
|
part, err := writer.CreateFormFile("File", "docker-compose.yml")
|
|
require.NoError(t, err)
|
|
|
|
_, err = part.Write([]byte("version: '3'\nservices:\n web:\n image: nginx"))
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, writer.Close())
|
|
|
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
|
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
|
r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole}))
|
|
|
|
rr := httptest.NewRecorder()
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.Nil(t, herr)
|
|
require.Equal(t, http.StatusOK, rr.Code)
|
|
|
|
var tmpl portainer.CustomTemplate
|
|
require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl))
|
|
require.Equal(t, "Uploaded Template", tmpl.Title)
|
|
require.Equal(t, filesystem.ComposeFileDefaultName, tmpl.EntryPoint)
|
|
|
|
err = ds.ViewTx(func(tx dataservices.DataStoreTx) error {
|
|
templates, err := tx.CustomTemplate().ReadAll()
|
|
require.NoError(t, err)
|
|
require.Len(t, templates, 1)
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_InvalidMethod(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
payload := customTemplateFromFileContentPayload{
|
|
Title: "My Template",
|
|
Description: "A test template",
|
|
FileContent: "version: '3'",
|
|
Type: portainer.DockerComposeStack,
|
|
Platform: portainer.CustomTemplatePlatformLinux,
|
|
}
|
|
|
|
r := createTemplateRequest(t, "invalid", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileContent_InvalidType(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
payload := customTemplateFromFileContentPayload{
|
|
Title: "My Template",
|
|
Description: "A test template",
|
|
FileContent: "version: '3'",
|
|
Type: 0,
|
|
Platform: portainer.CustomTemplatePlatformLinux,
|
|
}
|
|
|
|
r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileContent_Variables(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, ds, _ := newTestHandler(t)
|
|
|
|
payload := customTemplateFromFileContentPayload{
|
|
Title: "Template With Variables",
|
|
Description: "A test template",
|
|
FileContent: "version: '3'",
|
|
Type: portainer.DockerComposeStack,
|
|
Platform: portainer.CustomTemplatePlatformLinux,
|
|
Variables: []portainer.CustomTemplateVariableDefinition{
|
|
{Name: "IMAGE", Label: "Docker image"},
|
|
},
|
|
}
|
|
|
|
r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.Nil(t, herr)
|
|
require.Equal(t, http.StatusOK, rr.Code)
|
|
|
|
var tmpl portainer.CustomTemplate
|
|
require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl))
|
|
require.Len(t, tmpl.Variables, 1)
|
|
require.Equal(t, "IMAGE", tmpl.Variables[0].Name)
|
|
|
|
err := ds.ViewTx(func(tx dataservices.DataStoreTx) error {
|
|
stored, err := tx.CustomTemplate().Read(tmpl.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, stored.Variables, 1)
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileContent_InvalidVariables(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
payload := customTemplateFromFileContentPayload{
|
|
Title: "My Template",
|
|
Description: "A test template",
|
|
FileContent: "version: '3'",
|
|
Type: portainer.DockerComposeStack,
|
|
Platform: portainer.CustomTemplatePlatformLinux,
|
|
Variables: []portainer.CustomTemplateVariableDefinition{
|
|
{Label: "Missing name"},
|
|
},
|
|
}
|
|
|
|
r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileContent_EdgeTemplate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, ds, _ := newTestHandler(t)
|
|
|
|
payload := customTemplateFromFileContentPayload{
|
|
Title: "Edge Template",
|
|
Description: "For edge stacks",
|
|
FileContent: "version: '3'\nservices:\n web:\n image: nginx",
|
|
Type: portainer.DockerComposeStack,
|
|
Platform: portainer.CustomTemplatePlatformLinux,
|
|
EdgeTemplate: true,
|
|
}
|
|
|
|
r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.Nil(t, herr)
|
|
require.Equal(t, http.StatusOK, rr.Code)
|
|
|
|
var tmpl portainer.CustomTemplate
|
|
require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl))
|
|
require.True(t, tmpl.EdgeTemplate)
|
|
|
|
err := ds.ViewTx(func(tx dataservices.DataStoreTx) error {
|
|
stored, err := tx.CustomTemplate().Read(tmpl.ID)
|
|
require.NoError(t, err)
|
|
require.True(t, stored.EdgeTemplate)
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileUpload_MissingTitle(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
|
|
err := writer.WriteField("Description", "A description")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Type", "2")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Platform", "1")
|
|
require.NoError(t, err)
|
|
|
|
part, err := writer.CreateFormFile("File", "docker-compose.yml")
|
|
require.NoError(t, err)
|
|
|
|
_, err = part.Write([]byte("version: '3'"))
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, writer.Close())
|
|
|
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
|
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
|
r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole}))
|
|
|
|
rr := httptest.NewRecorder()
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileUpload_MissingDescription(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
|
|
err := writer.WriteField("Title", "My Template")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Type", "2")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Platform", "1")
|
|
require.NoError(t, err)
|
|
|
|
part, err := writer.CreateFormFile("File", "docker-compose.yml")
|
|
require.NoError(t, err)
|
|
|
|
_, err = part.Write([]byte("version: '3'"))
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, writer.Close())
|
|
|
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
|
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
|
r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole}))
|
|
|
|
rr := httptest.NewRecorder()
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileUpload_MissingFile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
|
|
err := writer.WriteField("Title", "My Template")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Description", "A description")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Type", "2")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Platform", "1")
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, writer.Close())
|
|
|
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
|
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
|
r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole}))
|
|
|
|
rr := httptest.NewRecorder()
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileUpload_InvalidType(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
|
|
err := writer.WriteField("Title", "My Template")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Description", "A description")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Type", "0")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Platform", "1")
|
|
require.NoError(t, err)
|
|
|
|
part, err := writer.CreateFormFile("File", "docker-compose.yml")
|
|
require.NoError(t, err)
|
|
|
|
_, err = part.Write([]byte("version: '3'"))
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, writer.Close())
|
|
|
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
|
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
|
r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole}))
|
|
|
|
rr := httptest.NewRecorder()
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileUpload_InvalidPlatform(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
|
|
err := writer.WriteField("Title", "My Template")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Description", "A description")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Type", "2")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Platform", "0")
|
|
require.NoError(t, err)
|
|
|
|
part, err := writer.CreateFormFile("File", "docker-compose.yml")
|
|
require.NoError(t, err)
|
|
|
|
_, err = part.Write([]byte("version: '3'"))
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, writer.Close())
|
|
|
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
|
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
|
r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole}))
|
|
|
|
rr := httptest.NewRecorder()
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileUpload_NoteWithImage(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
|
|
err := writer.WriteField("Title", "My Template")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Description", "A description")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Note", `Some note <img src="x" onerror="alert(1)">`)
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Type", "2")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Platform", "1")
|
|
require.NoError(t, err)
|
|
|
|
part, err := writer.CreateFormFile("File", "docker-compose.yml")
|
|
require.NoError(t, err)
|
|
|
|
_, err = part.Write([]byte("version: '3'"))
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, writer.Close())
|
|
|
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
|
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
|
r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole}))
|
|
|
|
rr := httptest.NewRecorder()
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileUpload_KubernetesIgnoresPlatform(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, ds, _ := newTestHandler(t)
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
|
|
err := writer.WriteField("Title", "K8s Upload Template")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Description", "A kubernetes template")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Type", "3")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Platform", "0")
|
|
require.NoError(t, err)
|
|
|
|
part, err := writer.CreateFormFile("File", "deployment.yml")
|
|
require.NoError(t, err)
|
|
|
|
_, err = part.Write([]byte("apiVersion: v1\nkind: Pod"))
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, writer.Close())
|
|
|
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
|
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
|
r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole}))
|
|
|
|
rr := httptest.NewRecorder()
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.Nil(t, herr)
|
|
require.Equal(t, http.StatusOK, rr.Code)
|
|
|
|
var tmpl portainer.CustomTemplate
|
|
require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl))
|
|
require.Equal(t, "K8s Upload Template", tmpl.Title)
|
|
require.Equal(t, filesystem.ComposeFileDefaultName, tmpl.EntryPoint)
|
|
|
|
err = ds.ViewTx(func(tx dataservices.DataStoreTx) error {
|
|
templates, err := tx.CustomTemplate().ReadAll()
|
|
require.NoError(t, err)
|
|
require.Len(t, templates, 1)
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileUpload_Variables(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, ds, _ := newTestHandler(t)
|
|
|
|
vars, err := json.Marshal([]portainer.CustomTemplateVariableDefinition{{Name: "IMAGE", Label: "Docker image"}})
|
|
require.NoError(t, err)
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
|
|
err = writer.WriteField("Title", "Template With Variables")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Description", "A description")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Type", "2")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Platform", "1")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Variables", string(vars))
|
|
require.NoError(t, err)
|
|
|
|
part, err := writer.CreateFormFile("File", "docker-compose.yml")
|
|
require.NoError(t, err)
|
|
|
|
_, err = part.Write([]byte("version: '3'"))
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, writer.Close())
|
|
|
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
|
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
|
r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole}))
|
|
|
|
rr := httptest.NewRecorder()
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.Nil(t, herr)
|
|
require.Equal(t, http.StatusOK, rr.Code)
|
|
|
|
var tmpl portainer.CustomTemplate
|
|
require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl))
|
|
require.Len(t, tmpl.Variables, 1)
|
|
require.Equal(t, "IMAGE", tmpl.Variables[0].Name)
|
|
|
|
err = ds.ViewTx(func(tx dataservices.DataStoreTx) error {
|
|
stored, err := tx.CustomTemplate().Read(tmpl.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, stored.Variables, 1)
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromFileUpload_InvalidVariables(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
|
|
err := writer.WriteField("Title", "My Template")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Description", "A description")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Type", "2")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Platform", "1")
|
|
require.NoError(t, err)
|
|
|
|
err = writer.WriteField("Variables", "not-valid-json")
|
|
require.NoError(t, err)
|
|
|
|
part, err := writer.CreateFormFile("File", "docker-compose.yml")
|
|
require.NoError(t, err)
|
|
|
|
_, err = part.Write([]byte("version: '3'"))
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, writer.Close())
|
|
|
|
r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body)
|
|
r.Header.Set("Content-Type", writer.FormDataContentType())
|
|
r = mux.SetURLVars(r, map[string]string{"method": "file"})
|
|
r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole}))
|
|
|
|
rr := httptest.NewRecorder()
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
type gitServiceCreatingFile struct {
|
|
portainer.GitService
|
|
}
|
|
|
|
func (g *gitServiceCreatingFile) CloneRepository(_ context.Context, destination, _, _, _, _ string, _ bool) error {
|
|
if err := os.MkdirAll(destination, 0700); err != nil {
|
|
return err
|
|
}
|
|
|
|
f, err := os.Create(filesystem.JoinPaths(destination, filesystem.ComposeFileDefaultName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return f.Close()
|
|
}
|
|
|
|
func (g *gitServiceCreatingFile) LatestCommitID(_ context.Context, _, _, _, _ string, _ bool) (string, error) {
|
|
return "deadbeef123", nil
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromRepository_Success(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, ds, _ := newTestHandler(t)
|
|
handler.GitService = &gitServiceCreatingFile{}
|
|
|
|
payload := customTemplateFromGitRepositoryPayload{
|
|
Title: "Git Template",
|
|
Description: "Created from git",
|
|
RepositoryURL: "https://github.com/example/repo",
|
|
Type: portainer.DockerComposeStack,
|
|
Platform: portainer.CustomTemplatePlatformLinux,
|
|
}
|
|
|
|
r := createTemplateRequest(t, "repository", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.Nil(t, herr)
|
|
require.Equal(t, http.StatusOK, rr.Code)
|
|
|
|
var tmpl portainer.CustomTemplate
|
|
require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl))
|
|
require.Equal(t, "Git Template", tmpl.Title)
|
|
require.NotNil(t, tmpl.Artifact)
|
|
require.Len(t, tmpl.Artifact.Files, 1)
|
|
require.Equal(t, "deadbeef123", tmpl.Artifact.Files[0].Hash)
|
|
|
|
err := ds.ViewTx(func(tx dataservices.DataStoreTx) error {
|
|
stored, err := tx.CustomTemplate().Read(tmpl.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, stored.Artifact)
|
|
|
|
src, err := tx.Source().Read(stored.Artifact.Files[0].SourceID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, portainer.SourceTypeGit, src.Type)
|
|
require.Equal(t, "https://github.com/example/repo", src.Git.URL)
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromRepository_DeduplicatesSource(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, ds, _ := newTestHandler(t)
|
|
handler.GitService = &gitServiceCreatingFile{}
|
|
|
|
makePayload := func(title string) customTemplateFromGitRepositoryPayload {
|
|
return customTemplateFromGitRepositoryPayload{
|
|
Title: title,
|
|
Description: "Created from git",
|
|
RepositoryURL: "https://github.com/example/repo",
|
|
Type: portainer.DockerComposeStack,
|
|
Platform: portainer.CustomTemplatePlatformLinux,
|
|
}
|
|
}
|
|
|
|
r1 := createTemplateRequest(t, "repository", makePayload("Template One"), 1, portainer.AdministratorRole)
|
|
rr1 := httptest.NewRecorder()
|
|
herr := handler.customTemplateCreate(rr1, r1)
|
|
require.Nil(t, herr)
|
|
|
|
r2 := createTemplateRequest(t, "repository", makePayload("Template Two"), 1, portainer.AdministratorRole)
|
|
rr2 := httptest.NewRecorder()
|
|
herr = handler.customTemplateCreate(rr2, r2)
|
|
require.Nil(t, herr)
|
|
|
|
err := ds.ViewTx(func(tx dataservices.DataStoreTx) error {
|
|
sources, err := tx.Source().ReadAll()
|
|
require.NoError(t, err)
|
|
require.Len(t, sources, 1, "two templates with the same URL must share one Source")
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromRepository_Validation_MissingTitle(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
payload := customTemplateFromGitRepositoryPayload{
|
|
Description: "A description",
|
|
RepositoryURL: "https://github.com/example/repo",
|
|
Type: portainer.DockerComposeStack,
|
|
Platform: portainer.CustomTemplatePlatformLinux,
|
|
}
|
|
|
|
r := createTemplateRequest(t, "repository", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromRepository_Validation_MissingDescription(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
payload := customTemplateFromGitRepositoryPayload{
|
|
Title: "My Template",
|
|
RepositoryURL: "https://github.com/example/repo",
|
|
Type: portainer.DockerComposeStack,
|
|
Platform: portainer.CustomTemplatePlatformLinux,
|
|
}
|
|
|
|
r := createTemplateRequest(t, "repository", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromRepository_Validation_InvalidURL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
payload := customTemplateFromGitRepositoryPayload{
|
|
Title: "My Template",
|
|
Description: "A description",
|
|
RepositoryURL: "http://",
|
|
Type: portainer.DockerComposeStack,
|
|
Platform: portainer.CustomTemplatePlatformLinux,
|
|
}
|
|
|
|
r := createTemplateRequest(t, "repository", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromRepository_Validation_AuthWithoutCredentials(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
payload := customTemplateFromGitRepositoryPayload{
|
|
Title: "My Template",
|
|
Description: "A description",
|
|
RepositoryURL: "https://github.com/example/repo",
|
|
RepositoryAuthentication: true,
|
|
Type: portainer.DockerComposeStack,
|
|
Platform: portainer.CustomTemplatePlatformLinux,
|
|
}
|
|
|
|
r := createTemplateRequest(t, "repository", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromRepository_Validation_InvalidPlatform(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
payload := customTemplateFromGitRepositoryPayload{
|
|
Title: "My Template",
|
|
Description: "A description",
|
|
RepositoryURL: "https://github.com/example/repo",
|
|
Type: portainer.DockerComposeStack,
|
|
Platform: 0,
|
|
}
|
|
|
|
r := createTemplateRequest(t, "repository", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromRepository_Validation_InvalidType(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
|
|
payload := customTemplateFromGitRepositoryPayload{
|
|
Title: "My Template",
|
|
Description: "A description",
|
|
RepositoryURL: "https://github.com/example/repo",
|
|
Type: 0,
|
|
Platform: portainer.CustomTemplatePlatformLinux,
|
|
}
|
|
|
|
r := createTemplateRequest(t, "repository", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromRepository_WithSourceID_Success(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, ds, _ := newTestHandler(t)
|
|
handler.GitService = &gitServiceCreatingFile{}
|
|
|
|
var srcID portainer.SourceID
|
|
require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
|
src := &portainer.Source{
|
|
Name: "example/repo",
|
|
Type: portainer.SourceTypeGit,
|
|
Git: &gittypes.RepoConfig{
|
|
URL: "https://github.com/example/repo",
|
|
},
|
|
}
|
|
err := tx.Source().Create(src)
|
|
require.NoError(t, err)
|
|
srcID = src.ID
|
|
return nil
|
|
}))
|
|
|
|
payload := customTemplateFromGitRepositoryPayload{
|
|
Title: "Source Template",
|
|
Description: "Created from source ID",
|
|
SourceID: srcID,
|
|
Type: portainer.DockerComposeStack,
|
|
Platform: portainer.CustomTemplatePlatformLinux,
|
|
}
|
|
|
|
r := createTemplateRequest(t, "repository", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.Nil(t, herr)
|
|
require.Equal(t, http.StatusOK, rr.Code)
|
|
|
|
var tmpl portainer.CustomTemplate
|
|
require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl))
|
|
require.NotNil(t, tmpl.Artifact)
|
|
require.Len(t, tmpl.Artifact.Files, 1)
|
|
require.Equal(t, srcID, tmpl.Artifact.Files[0].SourceID)
|
|
require.Equal(t, "deadbeef123", tmpl.Artifact.Files[0].Hash)
|
|
}
|
|
|
|
func TestCustomTemplateCreate_FromRepository_WithSourceID_NonExistentSource(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
handler, _, _ := newTestHandler(t)
|
|
handler.GitService = &gitServiceCreatingFile{}
|
|
|
|
payload := customTemplateFromGitRepositoryPayload{
|
|
Title: "Source Template",
|
|
Description: "Created from non-existent source ID",
|
|
SourceID: 999,
|
|
Type: portainer.DockerComposeStack,
|
|
Platform: portainer.CustomTemplatePlatformLinux,
|
|
}
|
|
|
|
r := createTemplateRequest(t, "repository", payload, 1, portainer.AdministratorRole)
|
|
rr := httptest.NewRecorder()
|
|
|
|
herr := handler.customTemplateCreate(rr, r)
|
|
require.NotNil(t, herr)
|
|
require.Equal(t, http.StatusInternalServerError, herr.StatusCode)
|
|
}
|