Commit 43a6876a authored by jackgr's avatar jackgr

Refactor manager/manager

parent e1afffbc
...@@ -19,15 +19,13 @@ package manager ...@@ -19,15 +19,13 @@ package manager
import ( import (
"fmt" "fmt"
"log" "log"
"net/url"
"regexp" "regexp"
"strings"
"time" "time"
"github.com/kubernetes/helm/cmd/manager/repository" "github.com/kubernetes/helm/cmd/manager/repository"
"github.com/kubernetes/helm/pkg/chart"
"github.com/kubernetes/helm/pkg/common" "github.com/kubernetes/helm/pkg/common"
"github.com/kubernetes/helm/pkg/registry" "github.com/kubernetes/helm/pkg/repo"
"github.com/kubernetes/helm/pkg/util"
) )
// Manager manages a persistent set of Deployments. // Manager manages a persistent set of Deployments.
...@@ -44,26 +42,20 @@ type Manager interface { ...@@ -44,26 +42,20 @@ type Manager interface {
GetManifest(deploymentName string, manifest string) (*common.Manifest, error) GetManifest(deploymentName string, manifest string) (*common.Manifest, error)
Expand(t *common.Template) (*common.Manifest, error) Expand(t *common.Template) (*common.Manifest, error)
// Types // Charts
ListTypes() ([]string, error) ListCharts() ([]string, error)
ListInstances(typeName string) ([]*common.TypeInstance, error) ListChartInstances(chartName string) ([]*common.ChartInstance, error)
GetRegistryForType(typeName string) (string, error) GetRepoForChart(chartName string) (string, error)
GetMetadataForType(typeName string) (string, error) GetMetadataForChart(chartName string) (*chart.Chartfile, error)
GetChart(chartName string) (*chart.Chart, error)
// Registries // Repo Charts
ListRegistries() ([]*common.Registry, error) ListRepoCharts(repoName string, regex *regexp.Regexp) ([]string, error)
CreateRegistry(pr *common.Registry) error GetChartForRepo(repoName, chartName string) (*chart.Chart, error)
GetRegistry(name string) (*common.Registry, error)
DeleteRegistry(name string) error
// Registry Types
ListRegistryTypes(registryName string, regex *regexp.Regexp) ([]registry.Type, error)
GetDownloadURLs(registryName string, t registry.Type) ([]*url.URL, error)
GetFile(registryName string, url string) (string, error)
// Credentials // Credentials
CreateCredential(name string, c *common.RegistryCredential) error CreateCredential(name string, c *repo.Credential) error
GetCredential(name string) (*common.RegistryCredential, error) GetCredential(name string) (*repo.Credential, error)
// Chart Repositories // Chart Repositories
ListChartRepos() ([]string, error) ListChartRepos() ([]string, error)
...@@ -75,21 +67,20 @@ type manager struct { ...@@ -75,21 +67,20 @@ type manager struct {
expander Expander expander Expander
deployer Deployer deployer Deployer
repository repository.Repository repository repository.Repository
registryProvider registry.RegistryProvider repoProvider repo.IRepoProvider
service common.RegistryService service repo.IRepoService
//TODO: add chart repo service //TODO: add chart repo service
credentialProvider common.CredentialProvider credentialProvider repo.ICredentialProvider
} }
// NewManager returns a new initialized Manager. // NewManager returns a new initialized Manager.
func NewManager(expander Expander, func NewManager(expander Expander,
deployer Deployer, deployer Deployer,
repository repository.Repository, repository repository.Repository,
registryProvider registry.RegistryProvider, repoProvider repo.IRepoProvider,
service common.RegistryService, service repo.IRepoService,
//TODO: add chart repo service credentialProvider repo.ICredentialProvider) Manager {
credentialProvider common.CredentialProvider) Manager { return &manager{expander, deployer, repository, repoProvider, service, credentialProvider}
return &manager{expander, deployer, repository, registryProvider, service, credentialProvider}
} }
// ListDeployments returns the list of deployments // ListDeployments returns the list of deployments
...@@ -190,7 +181,7 @@ func (m *manager) CreateDeployment(t *common.Template) (*common.Deployment, erro ...@@ -190,7 +181,7 @@ func (m *manager) CreateDeployment(t *common.Template) (*common.Deployment, erro
} }
// Finally update the type instances for this deployment. // Finally update the type instances for this deployment.
m.setTypeInstances(t.Name, manifest.Name, manifest.Layout) m.setChartInstances(t.Name, manifest.Name, manifest.Layout)
return m.repository.GetValidDeployment(t.Name) return m.repository.GetValidDeployment(t.Name)
} }
...@@ -210,20 +201,20 @@ func (m *manager) createManifest(t *common.Template) (*common.Manifest, error) { ...@@ -210,20 +201,20 @@ func (m *manager) createManifest(t *common.Template) (*common.Manifest, error) {
}, nil }, nil
} }
func (m *manager) setTypeInstances(deploymentName string, manifestName string, layout *common.Layout) { func (m *manager) setChartInstances(deploymentName string, manifestName string, layout *common.Layout) {
m.repository.ClearTypeInstancesForDeployment(deploymentName) m.repository.ClearChartInstancesForDeployment(deploymentName)
instances := make(map[string][]*common.TypeInstance) instances := make(map[string][]*common.ChartInstance)
for i, r := range layout.Resources { for i, r := range layout.Resources {
addTypeInstances(&instances, r, deploymentName, manifestName, fmt.Sprintf("$.resources[%d]", i)) addChartInstances(&instances, r, deploymentName, manifestName, fmt.Sprintf("$.resources[%d]", i))
} }
m.repository.AddTypeInstances(instances) m.repository.AddChartInstances(instances)
} }
func addTypeInstances(instances *map[string][]*common.TypeInstance, r *common.LayoutResource, deploymentName string, manifestName string, jsonPath string) { func addChartInstances(instances *map[string][]*common.ChartInstance, r *common.LayoutResource, deploymentName string, manifestName string, jsonPath string) {
// Add this resource. // Add this resource.
inst := &common.TypeInstance{ inst := &common.ChartInstance{
Name: r.Name, Name: r.Name,
Type: r.Type, Type: r.Type,
Deployment: deploymentName, Deployment: deploymentName,
...@@ -235,7 +226,7 @@ func addTypeInstances(instances *map[string][]*common.TypeInstance, r *common.La ...@@ -235,7 +226,7 @@ func addTypeInstances(instances *map[string][]*common.TypeInstance, r *common.La
// Add all sub resources if they exist. // Add all sub resources if they exist.
for i, sr := range r.Resources { for i, sr := range r.Resources {
addTypeInstances(instances, sr, deploymentName, manifestName, fmt.Sprintf("%s.resources[%d]", jsonPath, i)) addChartInstances(instances, sr, deploymentName, manifestName, fmt.Sprintf("%s.resources[%d]", jsonPath, i))
} }
} }
...@@ -286,7 +277,7 @@ func (m *manager) DeleteDeployment(name string, forget bool) (*common.Deployment ...@@ -286,7 +277,7 @@ func (m *manager) DeleteDeployment(name string, forget bool) (*common.Deployment
} }
// Finally remove the type instances for this deployment. // Finally remove the type instances for this deployment.
m.repository.ClearTypeInstancesForDeployment(name) m.repository.ClearChartInstancesForDeployment(name)
return d, nil return d, nil
} }
...@@ -319,7 +310,7 @@ func (m *manager) PutDeployment(name string, t *common.Template) (*common.Deploy ...@@ -319,7 +310,7 @@ func (m *manager) PutDeployment(name string, t *common.Template) (*common.Deploy
} }
// Finally update the type instances for this deployment. // Finally update the type instances for this deployment.
m.setTypeInstances(t.Name, manifest.Name, manifest.Layout) m.setChartInstances(t.Name, manifest.Name, manifest.Layout)
return m.repository.GetValidDeployment(t.Name) return m.repository.GetValidDeployment(t.Name)
} }
...@@ -336,59 +327,47 @@ func (m *manager) Expand(t *common.Template) (*common.Manifest, error) { ...@@ -336,59 +327,47 @@ func (m *manager) Expand(t *common.Template) (*common.Manifest, error) {
}, nil }, nil
} }
func (m *manager) ListTypes() ([]string, error) { func (m *manager) ListCharts() ([]string, error) {
return m.repository.ListTypes() return m.repository.ListCharts()
} }
func (m *manager) ListInstances(typeName string) ([]*common.TypeInstance, error) { func (m *manager) ListChartInstances(chartName string) ([]*common.ChartInstance, error) {
return m.repository.GetTypeInstances(typeName) return m.repository.GetChartInstances(chartName)
} }
// GetRegistryForType returns the registry where a type resides. // GetRepoForChart returns the repository where a chart resides.
func (m *manager) GetRegistryForType(typeName string) (string, error) { func (m *manager) GetRepoForChart(chartName string) (string, error) {
_, r, err := registry.GetDownloadURLs(m.registryProvider, typeName) _, r, err := m.repoProvider.GetChartByReference(chartName)
if err != nil { if err != nil {
return "", err return "", err
} }
return r.GetRegistryName(), nil return r.GetName(), nil
} }
// GetMetadataForType returns the metadata for type. // GetMetadataForChart returns the metadata for a chart.
func (m *manager) GetMetadataForType(typeName string) (string, error) { func (m *manager) GetMetadataForChart(chartName string) (*chart.Chartfile, error) {
URLs, r, err := registry.GetDownloadURLs(m.registryProvider, typeName) c, _, err := m.repoProvider.GetChartByReference(chartName)
if err != nil { if err != nil {
return "", err return nil, err
}
if len(URLs) < 1 {
return "", nil
} }
// If it's a chart, we want the provenance file return c.Chartfile(), nil
fPath := URLs[0] }
if !strings.Contains(fPath, ".prov") {
// It's not a chart, so we want the schema
fPath += ".schema"
}
metadata, err := getFileFromRegistry(fPath, r) // GetChart returns a chart.
func (m *manager) GetChart(chartName string) (*chart.Chart, error) {
c, _, err := m.repoProvider.GetChartByReference(chartName)
if err != nil { if err != nil {
return "", fmt.Errorf("cannot get metadata for type (%s): %s", typeName, err) return nil, err
} }
return metadata, nil return c, nil
}
// ListRegistries returns the list of registries
func (m *manager) ListRegistries() ([]*common.Registry, error) {
return m.service.List()
} }
// ListChartRepos returns the list of chart repositories // ListChartRepos returns the list of chart repositories
func (m *manager) ListChartRepos() ([]string, error) { func (m *manager) ListChartRepos() ([]string, error) {
//TODO: implement return m.service.List()
return nil, nil
} }
// AddChartRepo adds a chart repository to list of available chart repositories // AddChartRepo adds a chart repository to list of available chart repositories
...@@ -399,22 +378,17 @@ func (m *manager) AddChartRepo(name string) error { ...@@ -399,22 +378,17 @@ func (m *manager) AddChartRepo(name string) error {
// RemoveChartRepo removes a chart repository to list of available chart repositories // RemoveChartRepo removes a chart repository to list of available chart repositories
func (m *manager) RemoveChartRepo(name string) error { func (m *manager) RemoveChartRepo(name string) error {
//TODO: implement return m.service.Delete(name)
return nil
} }
func (m *manager) CreateRegistry(pr *common.Registry) error { func (m *manager) CreateRepo(pr repo.IRepo) error {
return m.service.Create(pr) return m.service.Create(pr)
} }
func (m *manager) GetRegistry(name string) (*common.Registry, error) { func (m *manager) GetRepo(name string) (repo.IRepo, error) {
return m.service.Get(name) return m.service.Get(name)
} }
func (m *manager) DeleteRegistry(name string) error {
return m.service.Delete(name)
}
func generateManifestName() string { func generateManifestName() string {
return fmt.Sprintf("manifest-%d", time.Now().UTC().UnixNano()) return fmt.Sprintf("manifest-%d", time.Now().UTC().UnixNano())
} }
...@@ -437,54 +411,33 @@ func getResourceErrors(c *common.Configuration) []string { ...@@ -437,54 +411,33 @@ func getResourceErrors(c *common.Configuration) []string {
return errs return errs
} }
// ListRegistryTypes lists types in a given registry whose string values // ListRepoCharts lists charts in a given repository whose names
// conform to the supplied regular expression, or all types, if the regular // conform to the supplied regular expression, or all charts, if the regular
// expression is nil. // expression is nil.
func (m *manager) ListRegistryTypes(registryName string, regex *regexp.Regexp) ([]registry.Type, error) { func (m *manager) ListRepoCharts(repoName string, regex *regexp.Regexp) ([]string, error) {
r, err := m.registryProvider.GetRegistryByName(registryName) r, err := m.repoProvider.GetRepoByName(repoName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return r.ListTypes(regex) return r.ListCharts(regex)
} }
// GetDownloadURLs returns the URLs required to download the contents // GetChartForRepo returns a chart from a given repository.
// of a given type in a given registry. func (m *manager) GetChartForRepo(repoName, chartName string) (*chart.Chart, error) {
func (m *manager) GetDownloadURLs(registryName string, t registry.Type) ([]*url.URL, error) { r, err := m.repoProvider.GetRepoByName(repoName)
r, err := m.registryProvider.GetRegistryByName(registryName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return r.GetDownloadURLs(t) return r.GetChart(chartName)
}
// GetFile returns a file from the backing registry
func (m *manager) GetFile(registryName string, url string) (string, error) {
r, err := m.registryProvider.GetRegistryByName(registryName)
if err != nil {
return "", err
}
return getFileFromRegistry(url, r)
}
func getFileFromRegistry(url string, r registry.Registry) (string, error) {
getter := util.NewHTTPClient(3, r, util.NewSleeper())
body, _, err := getter.Get(url)
if err != nil {
return "", err
}
return body, nil
} }
// CreateCredential creates a credential that can be used to authenticate to registry // CreateCredential creates a credential that can be used to authenticate to repository
func (m *manager) CreateCredential(name string, c *common.RegistryCredential) error { func (m *manager) CreateCredential(name string, c *repo.Credential) error {
return m.credentialProvider.SetCredential(name, c) return m.credentialProvider.SetCredential(name, c)
} }
func (m *manager) GetCredential(name string) (*common.RegistryCredential, error) { func (m *manager) GetCredential(name string) (*repo.Credential, error) {
return m.credentialProvider.GetCredential(name) return m.credentialProvider.GetCredential(name)
} }
...@@ -17,13 +17,13 @@ limitations under the License. ...@@ -17,13 +17,13 @@ limitations under the License.
package manager package manager
import ( import (
"github.com/kubernetes/helm/pkg/common"
"github.com/kubernetes/helm/pkg/repo"
"errors" "errors"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"github.com/kubernetes/helm/pkg/common"
"github.com/kubernetes/helm/pkg/registry"
) )
var template = common.Template{Name: "test", Content: "test"} var template = common.Template{Name: "test", Content: "test"}
...@@ -134,9 +134,9 @@ type repositoryStub struct { ...@@ -134,9 +134,9 @@ type repositoryStub struct {
ManifestSet map[string]*common.Manifest ManifestSet map[string]*common.Manifest
Deleted []string Deleted []string
GetValid []string GetValid []string
TypeInstances map[string][]string ChartInstances map[string][]string
TypeInstancesCleared bool ChartInstancesCleared bool
GetTypeInstancesCalled bool GetChartInstancesCalled bool
ListTypesCalled bool ListTypesCalled bool
DeploymentStates []*common.DeploymentState DeploymentStates []*common.DeploymentState
} }
...@@ -148,9 +148,9 @@ func (repository *repositoryStub) reset() { ...@@ -148,9 +148,9 @@ func (repository *repositoryStub) reset() {
repository.ManifestSet = make(map[string]*common.Manifest) repository.ManifestSet = make(map[string]*common.Manifest)
repository.Deleted = make([]string, 0) repository.Deleted = make([]string, 0)
repository.GetValid = make([]string, 0) repository.GetValid = make([]string, 0)
repository.TypeInstances = make(map[string][]string) repository.ChartInstances = make(map[string][]string)
repository.TypeInstancesCleared = false repository.ChartInstancesCleared = false
repository.GetTypeInstancesCalled = false repository.GetChartInstancesCalled = false
repository.ListTypesCalled = false repository.ListTypesCalled = false
repository.DeploymentStates = []*common.DeploymentState{} repository.DeploymentStates = []*common.DeploymentState{}
} }
...@@ -233,26 +233,26 @@ func (repository *repositoryStub) GetLatestManifest(d string) (*common.Manifest, ...@@ -233,26 +233,26 @@ func (repository *repositoryStub) GetLatestManifest(d string) (*common.Manifest,
} }
// Types. // Types.
func (repository *repositoryStub) ListTypes() ([]string, error) { func (repository *repositoryStub) ListCharts() ([]string, error) {
repository.ListTypesCalled = true repository.ListTypesCalled = true
return []string{}, nil return []string{}, nil
} }
func (repository *repositoryStub) GetTypeInstances(t string) ([]*common.TypeInstance, error) { func (repository *repositoryStub) GetChartInstances(t string) ([]*common.ChartInstance, error) {
repository.GetTypeInstancesCalled = true repository.GetChartInstancesCalled = true
return []*common.TypeInstance{}, nil return []*common.ChartInstance{}, nil
} }
func (repository *repositoryStub) ClearTypeInstancesForDeployment(d string) error { func (repository *repositoryStub) ClearChartInstancesForDeployment(d string) error {
repository.TypeInstancesCleared = true repository.ChartInstancesCleared = true
return nil return nil
} }
func (repository *repositoryStub) AddTypeInstances(is map[string][]*common.TypeInstance) error { func (repository *repositoryStub) AddChartInstances(is map[string][]*common.ChartInstance) error {
for t, instances := range is { for t, instances := range is {
for _, instance := range instances { for _, instance := range instances {
d := instance.Deployment d := instance.Deployment
repository.TypeInstances[d] = append(repository.TypeInstances[d], t) repository.ChartInstances[d] = append(repository.ChartInstances[d], t)
} }
} }
...@@ -264,10 +264,10 @@ func (repository *repositoryStub) Close() {} ...@@ -264,10 +264,10 @@ func (repository *repositoryStub) Close() {}
var testExpander = &expanderStub{} var testExpander = &expanderStub{}
var testRepository = newRepositoryStub() var testRepository = newRepositoryStub()
var testDeployer = newDeployerStub() var testDeployer = newDeployerStub()
var testRegistryService = registry.NewInmemRegistryService() var testRepoService = repo.NewInmemRepoService()
var testCredentialProvider = registry.NewInmemCredentialProvider() var testCredentialProvider = repo.NewInmemCredentialProvider()
var testProvider = registry.NewRegistryProvider(nil, registry.NewTestGithubRegistryProvider("", nil), registry.NewTestGCSRegistryProvider("", nil), testCredentialProvider) var testProvider = repo.NewRepoProvider(nil, repo.NewGCSRepoProvider(testCredentialProvider), testCredentialProvider)
var testManager = NewManager(testExpander, testDeployer, testRepository, testProvider, testRegistryService, testCredentialProvider) var testManager = NewManager(testExpander, testDeployer, testRepository, testProvider, testRepoService, testCredentialProvider)
func TestListDeployments(t *testing.T) { func TestListDeployments(t *testing.T) {
testRepository.reset() testRepository.reset()
...@@ -363,12 +363,12 @@ func TestCreateDeployment(t *testing.T) { ...@@ -363,12 +363,12 @@ func TestCreateDeployment(t *testing.T) {
t.Fatal("CreateDeployment success did not mark deployment as deployed") t.Fatal("CreateDeployment success did not mark deployment as deployed")
} }
if !testRepository.TypeInstancesCleared { if !testRepository.ChartInstancesCleared {
t.Fatal("Repository did not clear type instances during creation") t.Fatal("Repository did not clear type instances during creation")
} }
if !reflect.DeepEqual(testRepository.TypeInstances, typeInstMap) { if !reflect.DeepEqual(testRepository.ChartInstances, typeInstMap) {
t.Fatalf("Unexpected type instances after CreateDeployment: %s", testRepository.TypeInstances) t.Fatalf("Unexpected type instances after CreateDeployment: %s", testRepository.ChartInstances)
} }
} }
...@@ -397,7 +397,7 @@ func TestCreateDeploymentCreationFailure(t *testing.T) { ...@@ -397,7 +397,7 @@ func TestCreateDeploymentCreationFailure(t *testing.T) {
"Received: %v, %s. Expected: %s, %s.", d, err, "nil", errTest) "Received: %v, %s. Expected: %s, %s.", d, err, "nil", errTest)
} }
if testRepository.TypeInstancesCleared { if testRepository.ChartInstancesCleared {
t.Fatal("Unexpected change to type instances during CreateDeployment failure.") t.Fatal("Unexpected change to type instances during CreateDeployment failure.")
} }
} }
...@@ -437,7 +437,7 @@ func TestCreateDeploymentCreationResourceFailure(t *testing.T) { ...@@ -437,7 +437,7 @@ func TestCreateDeploymentCreationResourceFailure(t *testing.T) {
"Received: %v, %v. Expected: %v, %v.", d, err, &deployment, "nil") "Received: %v, %v. Expected: %v, %v.", d, err, &deployment, "nil")
} }
if !testRepository.TypeInstancesCleared { if !testRepository.ChartInstancesCleared {
t.Fatal("Repository did not clear type instances during creation") t.Fatal("Repository did not clear type instances during creation")
} }
} }
...@@ -486,7 +486,7 @@ func TestDeleteDeploymentForget(t *testing.T) { ...@@ -486,7 +486,7 @@ func TestDeleteDeploymentForget(t *testing.T) {
} }
} }
if !testRepository.TypeInstancesCleared { if !testRepository.ChartInstancesCleared {
t.Fatal("Expected type instances to be cleared during DeleteDeployment.") t.Fatal("Expected type instances to be cleared during DeleteDeployment.")
} }
} }
...@@ -521,29 +521,29 @@ func TestExpand(t *testing.T) { ...@@ -521,29 +521,29 @@ func TestExpand(t *testing.T) {
func TestListTypes(t *testing.T) { func TestListTypes(t *testing.T) {
testRepository.reset() testRepository.reset()
testManager.ListTypes() testManager.ListCharts()
if !testRepository.ListTypesCalled { if !testRepository.ListTypesCalled {
t.Fatal("expected repository ListTypes() call.") t.Fatal("expected repository ListCharts() call.")
} }
} }
func TestListInstances(t *testing.T) { func TestListInstances(t *testing.T) {
testRepository.reset() testRepository.reset()
testManager.ListInstances("all") testManager.ListChartInstances("all")
if !testRepository.GetTypeInstancesCalled { if !testRepository.GetChartInstancesCalled {
t.Fatal("expected repository GetTypeInstances() call.") t.Fatal("expected repository GetChartInstances() call.")
} }
} }
// TODO(jackgr): Implement TestListRegistryTypes // TODO(jackgr): Implement TestListRepoCharts
func TestListRegistryTypes(t *testing.T) { func TestListRepoCharts(t *testing.T) {
/* /*
types, err := testManager.ListRegistryTypes("", nil) types, err := testManager.ListRepoCharts("", nil)
if err != nil { if err != nil {
t.Fatalf("cannot list registry types: %s", err) t.Fatalf("cannot list repository types: %s", err)
} }
*/ */
} }
...@@ -551,7 +551,7 @@ func TestListRegistryTypes(t *testing.T) { ...@@ -551,7 +551,7 @@ func TestListRegistryTypes(t *testing.T) {
// TODO(jackgr): Implement TestGetDownloadURLs // TODO(jackgr): Implement TestGetDownloadURLs
func TestGetDownloadURLs(t *testing.T) { func TestGetDownloadURLs(t *testing.T) {
/* /*
urls, err := testManager.GetDownloadURLs("", registry.Type{}) urls, err := testManager.GetDownloadURLs("", repo.Type{})
if err != nil { if err != nil {
t.Fatalf("cannot list get download urls: %s", err) t.Fatalf("cannot list get download urls: %s", err)
} }
......
...@@ -170,9 +170,9 @@ type Resource struct { ...@@ -170,9 +170,9 @@ type Resource struct {
State *ResourceState `json:"state,omitempty"` State *ResourceState `json:"state,omitempty"`
} }
// TypeInstance defines the metadata for an instantiation of a template type // ChartInstance defines the metadata for an instantiation of a template type
// in a deployment. // in a deployment.
type TypeInstance struct { type ChartInstance struct {
Name string `json:"name"` // instance name Name string `json:"name"` // instance name
Type string `json:"type"` // instance type Type string `json:"type"` // instance type
Deployment string `json:"deployment"` // deployment name Deployment string `json:"deployment"` // deployment name
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment