portainer/api/datastore/datastore_test.go

417 lines
12 KiB
Go

package datastore
import (
"fmt"
"runtime"
"strings"
"testing"
"github.com/dchest/uniuri"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api/crypto"
"github.com/stretchr/testify/assert"
)
const (
adminUsername = "admin"
adminPassword = "password"
standardUsername = "standard"
standardPassword = "password"
agentOnDockerEnvironmentUrl = "tcp://192.168.167.207:30775"
edgeAgentOnKubernetesEnvironmentUrl = "tcp://192.168.167.207"
kubernetesLocalEnvironmentUrl = "https://kubernetes.default.svc"
)
// TestStoreFull an eventually comprehensive set of tests for the Store.
// The idea is what we write to the store, we should read back.
func TestStoreFull(t *testing.T) {
_, store := MustNewTestStore(t, true, true)
testCases := map[string]func(t *testing.T){
"User Accounts": func(t *testing.T) {
store.testUserAccounts(t)
},
"Environments": func(t *testing.T) {
store.testEnvironments(t)
},
"Settings": func(t *testing.T) {
store.testSettings(t)
},
"SSL Settings": func(t *testing.T) {
store.testSSLSettings(t)
},
"Tunnel Server": func(t *testing.T) {
store.testTunnelServer(t)
},
"Custom Templates": func(t *testing.T) {
store.testCustomTemplates(t)
},
"Registries": func(t *testing.T) {
store.testRegistries(t)
},
"Resource Control": func(t *testing.T) {
store.testResourceControl(t)
},
"Schedules": func(t *testing.T) {
store.testSchedules(t)
},
"Tags": func(t *testing.T) {
store.testTags(t)
},
// "Test Title": func(t *testing.T) {
// },
}
for name, test := range testCases {
t.Run(name, test)
}
}
func (store *Store) testEnvironments(t *testing.T) {
id := store.CreateEndpoint(t, "local", portainer.KubernetesLocalEnvironment, "", true)
store.CreateEndpointRelation(id)
id = store.CreateEndpoint(t, "agent", portainer.AgentOnDockerEnvironment, agentOnDockerEnvironmentUrl, true)
store.CreateEndpointRelation(id)
id = store.CreateEndpoint(t, "edge", portainer.EdgeAgentOnKubernetesEnvironment, edgeAgentOnKubernetesEnvironmentUrl, true)
store.CreateEndpointRelation(id)
}
func newEndpoint(endpointType portainer.EndpointType, id portainer.EndpointID, name, URL string, TLS bool) *portainer.Endpoint {
endpoint := &portainer.Endpoint{
ID: id,
Name: name,
URL: URL,
Type: endpointType,
GroupID: portainer.EndpointGroupID(1),
PublicURL: "",
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.DockerSnapshot{},
Kubernetes: portainer.KubernetesDefault(),
}
if TLS {
endpoint.TLSConfig = portainer.TLSConfiguration{
TLS: true,
TLSSkipVerify: true,
}
}
return endpoint
}
func setEndpointAuthorizations(endpoint *portainer.Endpoint) {
endpoint.SecuritySettings = portainer.EndpointSecuritySettings{
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
AllowSysctlSettingForRegularUsers: true,
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowHostNamespaceForRegularUsers: true,
AllowContainerCapabilitiesForRegularUsers: true,
AllowDeviceMappingForRegularUsers: true,
AllowStackManagementForRegularUsers: true,
}
}
func (store *Store) CreateEndpoint(t *testing.T, name string, endpointType portainer.EndpointType, URL string, tls bool) portainer.EndpointID {
is := assert.New(t)
var expectedEndpoint *portainer.Endpoint
id := portainer.EndpointID(store.Endpoint().GetNextIdentifier())
switch endpointType {
case portainer.DockerEnvironment:
if URL == "" {
URL = "unix:///var/run/docker.sock"
if runtime.GOOS == "windows" {
URL = "npipe:////./pipe/docker_engine"
}
}
expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
case portainer.AgentOnDockerEnvironment:
expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
case portainer.AgentOnKubernetesEnvironment:
URL = strings.TrimPrefix(URL, "tcp://")
expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
case portainer.EdgeAgentOnKubernetesEnvironment:
cs := chisel.NewService(store, nil, nil)
expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
edgeKey := cs.GenerateEdgeKey(URL, "", int(id))
expectedEndpoint.EdgeKey = edgeKey
store.testTunnelServer(t)
case portainer.KubernetesLocalEnvironment:
if URL == "" {
URL = kubernetesLocalEnvironmentUrl
}
expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
}
setEndpointAuthorizations(expectedEndpoint)
store.Endpoint().Create(expectedEndpoint)
endpoint, err := store.Endpoint().Endpoint(id)
is.NoError(err, "Endpoint() should not return an error")
is.Equal(expectedEndpoint, endpoint, "endpoint should be the same")
return endpoint.ID
}
func (store *Store) CreateEndpointRelation(id portainer.EndpointID) {
relation := &portainer.EndpointRelation{
EndpointID: id,
EdgeStacks: map[portainer.EdgeStackID]bool{},
}
store.EndpointRelation().Create(relation)
}
func (store *Store) testSSLSettings(t *testing.T) {
is := assert.New(t)
ssl := &portainer.SSLSettings{
CertPath: "/data/certs/cert.pem",
HTTPEnabled: true,
KeyPath: "/data/certs/key.pem",
SelfSigned: true,
}
store.SSLSettings().UpdateSettings(ssl)
settings, err := store.SSLSettings().Settings()
is.NoError(err, "Get sslsettings should succeed")
is.Equal(ssl, settings, "Stored SSLSettings should be the same as what is read out")
}
func (store *Store) testTunnelServer(t *testing.T) {
is := assert.New(t)
expectPrivateKeySeed := uniuri.NewLen(16)
err := store.TunnelServer().UpdateInfo(&portainer.TunnelServerInfo{PrivateKeySeed: expectPrivateKeySeed})
is.NoError(err, "UpdateInfo should have succeeded")
serverInfo, err := store.TunnelServer().Info()
is.NoError(err, "Info should have succeeded")
is.Equal(expectPrivateKeySeed, serverInfo.PrivateKeySeed, "hashed passwords should not differ")
}
// add users, read them back and check the details are unchanged
func (store *Store) testUserAccounts(t *testing.T) {
is := assert.New(t)
err := store.createAccount(adminUsername, adminPassword, portainer.AdministratorRole)
is.NoError(err, "CreateAccount should succeed")
store.checkAccount(adminUsername, adminPassword, portainer.AdministratorRole)
is.NoError(err, "Account failure")
err = store.createAccount(standardUsername, standardPassword, portainer.StandardUserRole)
is.NoError(err, "CreateAccount should succeed")
store.checkAccount(standardUsername, standardPassword, portainer.StandardUserRole)
is.NoError(err, "Account failure")
}
// create an account with the provided details
func (store *Store) createAccount(username, password string, role portainer.UserRole) error {
var err error
user := &portainer.User{Username: username, Role: role}
// encrypt the password
cs := &crypto.Service{}
user.Password, err = cs.Hash(password)
if err != nil {
return err
}
err = store.User().Create(user)
if err != nil {
return err
}
return nil
}
func (store *Store) checkAccount(username, expectPassword string, expectRole portainer.UserRole) error {
// Read the account for username. Check password and role is what we expect
user, err := store.User().UserByUsername(username)
if err != nil {
return errors.Wrap(err, "failed to find user")
}
if user.Username != username || user.Role != expectRole {
return fmt.Errorf("%s user details do not match", user.Username)
}
// Check the password
cs := &crypto.Service{}
expectPasswordHash, err := cs.Hash(expectPassword)
if err != nil {
return errors.Wrap(err, "hash failed")
}
if user.Password != expectPasswordHash {
return fmt.Errorf("%s user password hash failure", user.Username)
}
return nil
}
func (store *Store) testSettings(t *testing.T) {
is := assert.New(t)
// since many settings are default and basically nil, I'm going to update some and read them back
expectedSettings, err := store.Settings().Settings()
is.NoError(err, "Settings() should not return an error")
expectedSettings.TemplatesURL = "http://portainer.io/application-templates"
expectedSettings.HelmRepositoryURL = "http://portainer.io/helm-repository"
expectedSettings.EdgeAgentCheckinInterval = 60
expectedSettings.AuthenticationMethod = portainer.AuthenticationLDAP
expectedSettings.LDAPSettings = portainer.LDAPSettings{
AnonymousMode: true,
StartTLS: true,
AutoCreateUsers: true,
Password: "random",
}
expectedSettings.SnapshotInterval = "10m"
err = store.Settings().UpdateSettings(expectedSettings)
is.NoError(err, "UpdateSettings() should succeed")
settings, err := store.Settings().Settings()
is.NoError(err, "Settings() should not return an error")
is.Equal(expectedSettings, settings, "stored settings should match")
}
func (store *Store) testCustomTemplates(t *testing.T) {
is := assert.New(t)
customTemplate := store.CustomTemplate()
is.NotNil(customTemplate, "customTemplate Service shouldn't be nil")
expectedTemplate := &portainer.CustomTemplate{
ID: portainer.CustomTemplateID(customTemplate.GetNextIdentifier()),
Title: "Custom Title",
Description: "Custom Template Description",
ProjectPath: "/data/custom_template/1",
Note: "A note about this custom template",
EntryPoint: "docker-compose.yaml",
CreatedByUserID: 10,
}
customTemplate.Create(expectedTemplate)
actualTemplate, err := customTemplate.Read(expectedTemplate.ID)
is.NoError(err, "CustomTemplate should not return an error")
is.Equal(expectedTemplate, actualTemplate, "expected and actual template do not match")
}
func (store *Store) testRegistries(t *testing.T) {
is := assert.New(t)
regService := store.RegistryService
is.NotNil(regService, "RegistryService shouldn't be nil")
reg1 := &portainer.Registry{
ID: 1,
Type: portainer.DockerHubRegistry,
Name: "Dockerhub Registry Test",
}
reg2 := &portainer.Registry{
ID: 2,
Type: portainer.GitlabRegistry,
Name: "Gitlab Registry Test",
Gitlab: portainer.GitlabRegistryData{
ProjectID: 12345,
InstanceURL: "http://gitlab.com/12345",
ProjectPath: "mytestproject",
},
}
err := regService.Create(reg1)
is.NoError(err)
err = regService.Create(reg2)
is.NoError(err)
actualReg1, err := regService.Read(reg1.ID)
is.NoError(err)
is.Equal(reg1, actualReg1, "registries differ")
actualReg2, err := regService.Read(reg2.ID)
is.NoError(err)
is.Equal(reg2, actualReg2, "registries differ")
}
func (store *Store) testResourceControl(t *testing.T) {
// is := assert.New(t)
// resControl := store.ResourceControl()
// ctrl := &portainer.ResourceControl{
// }
// resControl().Create()
}
func (store *Store) testSchedules(t *testing.T) {
is := assert.New(t)
schedule := store.ScheduleService
s := &portainer.Schedule{
ID: portainer.ScheduleID(schedule.GetNextIdentifier()),
Name: "My Custom Schedule 1",
CronExpression: "*/5 * * * * portainer /bin/sh -c echo 'hello world'",
}
err := schedule.CreateSchedule(s)
is.NoError(err, "CreateSchedule should succeed")
actual, err := schedule.Schedule(s.ID)
is.NoError(err, "schedule should be found")
is.Equal(s, actual, "schedules differ")
}
func (store *Store) testTags(t *testing.T) {
is := assert.New(t)
tags := store.TagService
tag1 := &portainer.Tag{
ID: 1,
Name: "Tag 1",
}
tag2 := &portainer.Tag{
ID: 2,
Name: "Tag 1",
}
err := tags.Create(tag1)
is.NoError(err, "Tags.Create should succeed")
err = tags.Create(tag2)
is.NoError(err, "Tags.Create should succeed")
actual, err := tags.Read(tag1.ID)
is.NoError(err, "tag1 should be found")
is.Equal(tag1, actual, "tags differ")
actual, err = tags.Read(tag2.ID)
is.NoError(err, "tag2 should be found")
is.Equal(tag2, actual, "tags differ")
}