mirror of https://github.com/docker/cli.git
Merge pull request #721 from silvin-lubecki/kube
Add kubernetes support to docker/cli
This commit is contained in:
commit
e708c900f8
|
@ -44,6 +44,7 @@ type Cli interface {
|
||||||
ServerInfo() ServerInfo
|
ServerInfo() ServerInfo
|
||||||
ClientInfo() ClientInfo
|
ClientInfo() ClientInfo
|
||||||
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
||||||
|
DefaultVersion() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// DockerCli is an instance the docker command line client.
|
// DockerCli is an instance the docker command line client.
|
||||||
|
@ -135,9 +136,11 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Experimental field")
|
return errors.Wrap(err, "Experimental field")
|
||||||
}
|
}
|
||||||
|
orchestrator := GetOrchestrator(hasExperimental, opts.Common.Orchestrator, cli.configFile.Orchestrator)
|
||||||
cli.clientInfo = ClientInfo{
|
cli.clientInfo = ClientInfo{
|
||||||
DefaultVersion: cli.client.ClientVersion(),
|
DefaultVersion: cli.client.ClientVersion(),
|
||||||
HasExperimental: hasExperimental,
|
HasExperimental: hasExperimental,
|
||||||
|
Orchestrator: orchestrator,
|
||||||
}
|
}
|
||||||
cli.initializeFromClient()
|
cli.initializeFromClient()
|
||||||
return nil
|
return nil
|
||||||
|
@ -203,6 +206,12 @@ type ServerInfo struct {
|
||||||
type ClientInfo struct {
|
type ClientInfo struct {
|
||||||
HasExperimental bool
|
HasExperimental bool
|
||||||
DefaultVersion string
|
DefaultVersion string
|
||||||
|
Orchestrator Orchestrator
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasKubernetes checks if kubernetes orchestrator is enabled
|
||||||
|
func (c ClientInfo) HasKubernetes() bool {
|
||||||
|
return c.HasExperimental && c.Orchestrator == OrchestratorKubernetes
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
|
// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
|
||||||
|
|
|
@ -170,6 +170,110 @@ func TestExperimentalCLI(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOrchestratorSwitch(t *testing.T) {
|
||||||
|
defaultVersion := "v0.00"
|
||||||
|
|
||||||
|
var testcases = []struct {
|
||||||
|
doc string
|
||||||
|
configfile string
|
||||||
|
envOrchestrator string
|
||||||
|
flagOrchestrator string
|
||||||
|
expectedOrchestrator string
|
||||||
|
expectedKubernetes bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
doc: "default",
|
||||||
|
configfile: `{
|
||||||
|
"experimental": "enabled"
|
||||||
|
}`,
|
||||||
|
expectedOrchestrator: "swarm",
|
||||||
|
expectedKubernetes: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "kubernetesIsExperimental",
|
||||||
|
configfile: `{
|
||||||
|
"experimental": "disabled",
|
||||||
|
"orchestrator": "kubernetes"
|
||||||
|
}`,
|
||||||
|
envOrchestrator: "kubernetes",
|
||||||
|
flagOrchestrator: "kubernetes",
|
||||||
|
expectedOrchestrator: "swarm",
|
||||||
|
expectedKubernetes: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "kubernetesConfigFile",
|
||||||
|
configfile: `{
|
||||||
|
"experimental": "enabled",
|
||||||
|
"orchestrator": "kubernetes"
|
||||||
|
}`,
|
||||||
|
expectedOrchestrator: "kubernetes",
|
||||||
|
expectedKubernetes: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "kubernetesEnv",
|
||||||
|
configfile: `{
|
||||||
|
"experimental": "enabled"
|
||||||
|
}`,
|
||||||
|
envOrchestrator: "kubernetes",
|
||||||
|
expectedOrchestrator: "kubernetes",
|
||||||
|
expectedKubernetes: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "kubernetesFlag",
|
||||||
|
configfile: `{
|
||||||
|
"experimental": "enabled"
|
||||||
|
}`,
|
||||||
|
flagOrchestrator: "kubernetes",
|
||||||
|
expectedOrchestrator: "kubernetes",
|
||||||
|
expectedKubernetes: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "envOverridesConfigFile",
|
||||||
|
configfile: `{
|
||||||
|
"experimental": "enabled",
|
||||||
|
"orchestrator": "kubernetes"
|
||||||
|
}`,
|
||||||
|
envOrchestrator: "swarm",
|
||||||
|
expectedOrchestrator: "swarm",
|
||||||
|
expectedKubernetes: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "flagOverridesEnv",
|
||||||
|
configfile: `{
|
||||||
|
"experimental": "enabled"
|
||||||
|
}`,
|
||||||
|
envOrchestrator: "kubernetes",
|
||||||
|
flagOrchestrator: "swarm",
|
||||||
|
expectedOrchestrator: "swarm",
|
||||||
|
expectedKubernetes: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testcase := range testcases {
|
||||||
|
t.Run(testcase.doc, func(t *testing.T) {
|
||||||
|
dir := fs.NewDir(t, testcase.doc, fs.WithFile("config.json", testcase.configfile))
|
||||||
|
defer dir.Remove()
|
||||||
|
apiclient := &fakeClient{
|
||||||
|
version: defaultVersion,
|
||||||
|
}
|
||||||
|
if testcase.envOrchestrator != "" {
|
||||||
|
defer patchEnvVariable(t, "DOCKER_ORCHESTRATOR", testcase.envOrchestrator)()
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := &DockerCli{client: apiclient, err: os.Stderr}
|
||||||
|
cliconfig.SetDir(dir.Path())
|
||||||
|
options := flags.NewClientOptions()
|
||||||
|
if testcase.flagOrchestrator != "" {
|
||||||
|
options.Common.Orchestrator = testcase.flagOrchestrator
|
||||||
|
}
|
||||||
|
err := cli.Initialize(options)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, testcase.expectedKubernetes, cli.ClientInfo().HasKubernetes())
|
||||||
|
assert.Equal(t, testcase.expectedOrchestrator, string(cli.ClientInfo().Orchestrator))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetClientWithPassword(t *testing.T) {
|
func TestGetClientWithPassword(t *testing.T) {
|
||||||
expected := "password"
|
expected := "password"
|
||||||
|
|
||||||
|
|
|
@ -10,11 +10,14 @@ import (
|
||||||
// NewConfigCommand returns a cobra command for `config` subcommands
|
// NewConfigCommand returns a cobra command for `config` subcommands
|
||||||
func NewConfigCommand(dockerCli command.Cli) *cobra.Command {
|
func NewConfigCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "config",
|
Use: "config",
|
||||||
Short: "Manage Docker configs",
|
Short: "Manage Docker configs",
|
||||||
Args: cli.NoArgs,
|
Args: cli.NoArgs,
|
||||||
RunE: command.ShowHelp(dockerCli.Err()),
|
RunE: command.ShowHelp(dockerCli.Err()),
|
||||||
Annotations: map[string]string{"version": "1.30"},
|
Annotations: map[string]string{
|
||||||
|
"version": "1.30",
|
||||||
|
"swarm": "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newConfigListCommand(dockerCli),
|
newConfigListCommand(dockerCli),
|
||||||
|
|
|
@ -14,11 +14,14 @@ import (
|
||||||
// NewNodeCommand returns a cobra command for `node` subcommands
|
// NewNodeCommand returns a cobra command for `node` subcommands
|
||||||
func NewNodeCommand(dockerCli command.Cli) *cobra.Command {
|
func NewNodeCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "node",
|
Use: "node",
|
||||||
Short: "Manage Swarm nodes",
|
Short: "Manage Swarm nodes",
|
||||||
Args: cli.NoArgs,
|
Args: cli.NoArgs,
|
||||||
RunE: command.ShowHelp(dockerCli.Err()),
|
RunE: command.ShowHelp(dockerCli.Err()),
|
||||||
Annotations: map[string]string{"version": "1.24"},
|
Annotations: map[string]string{
|
||||||
|
"version": "1.24",
|
||||||
|
"swarm": "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newDemoteCommand(dockerCli),
|
newDemoteCommand(dockerCli),
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Orchestrator type acts as an enum describing supported orchestrators.
|
||||||
|
type Orchestrator string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// OrchestratorKubernetes orchestrator
|
||||||
|
OrchestratorKubernetes = Orchestrator("kubernetes")
|
||||||
|
// OrchestratorSwarm orchestrator
|
||||||
|
OrchestratorSwarm = Orchestrator("swarm")
|
||||||
|
orchestratorUnset = Orchestrator("unset")
|
||||||
|
|
||||||
|
defaultOrchestrator = OrchestratorSwarm
|
||||||
|
envVarDockerOrchestrator = "DOCKER_ORCHESTRATOR"
|
||||||
|
)
|
||||||
|
|
||||||
|
func normalize(flag string) Orchestrator {
|
||||||
|
switch flag {
|
||||||
|
case "kubernetes", "k8s":
|
||||||
|
return OrchestratorKubernetes
|
||||||
|
case "swarm", "swarmkit":
|
||||||
|
return OrchestratorSwarm
|
||||||
|
default:
|
||||||
|
return orchestratorUnset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrchestrator checks DOCKER_ORCHESTRATOR environment variable and configuration file
|
||||||
|
// orchestrator value and returns user defined Orchestrator.
|
||||||
|
func GetOrchestrator(isExperimental bool, flagValue, value string) Orchestrator {
|
||||||
|
// Non experimental CLI has kubernetes disabled
|
||||||
|
if !isExperimental {
|
||||||
|
return defaultOrchestrator
|
||||||
|
}
|
||||||
|
// Check flag
|
||||||
|
if o := normalize(flagValue); o != orchestratorUnset {
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
// Check environment variable
|
||||||
|
env := os.Getenv(envVarDockerOrchestrator)
|
||||||
|
if o := normalize(env); o != orchestratorUnset {
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
// Check specified orchestrator
|
||||||
|
if o := normalize(value); o != orchestratorUnset {
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
if value != "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "Specified orchestrator %q is invalid. Please use either kubernetes or swarm\n", value)
|
||||||
|
}
|
||||||
|
// Nothing set, use default orchestrator
|
||||||
|
return defaultOrchestrator
|
||||||
|
}
|
|
@ -10,11 +10,14 @@ import (
|
||||||
// NewSecretCommand returns a cobra command for `secret` subcommands
|
// NewSecretCommand returns a cobra command for `secret` subcommands
|
||||||
func NewSecretCommand(dockerCli command.Cli) *cobra.Command {
|
func NewSecretCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "secret",
|
Use: "secret",
|
||||||
Short: "Manage Docker secrets",
|
Short: "Manage Docker secrets",
|
||||||
Args: cli.NoArgs,
|
Args: cli.NoArgs,
|
||||||
RunE: command.ShowHelp(dockerCli.Err()),
|
RunE: command.ShowHelp(dockerCli.Err()),
|
||||||
Annotations: map[string]string{"version": "1.25"},
|
Annotations: map[string]string{
|
||||||
|
"version": "1.25",
|
||||||
|
"swarm": "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newSecretListCommand(dockerCli),
|
newSecretListCommand(dockerCli),
|
||||||
|
|
|
@ -10,11 +10,14 @@ import (
|
||||||
// NewServiceCommand returns a cobra command for `service` subcommands
|
// NewServiceCommand returns a cobra command for `service` subcommands
|
||||||
func NewServiceCommand(dockerCli command.Cli) *cobra.Command {
|
func NewServiceCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "service",
|
Use: "service",
|
||||||
Short: "Manage services",
|
Short: "Manage services",
|
||||||
Args: cli.NoArgs,
|
Args: cli.NoArgs,
|
||||||
RunE: command.ShowHelp(dockerCli.Err()),
|
RunE: command.ShowHelp(dockerCli.Err()),
|
||||||
Annotations: map[string]string{"version": "1.24"},
|
Annotations: map[string]string{
|
||||||
|
"version": "1.24",
|
||||||
|
"swarm": "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newCreateCommand(dockerCli),
|
newCreateCommand(dockerCli),
|
||||||
|
|
|
@ -18,10 +18,17 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newDeployCommand(dockerCli),
|
newDeployCommand(dockerCli),
|
||||||
newListCommand(dockerCli),
|
newListCommand(dockerCli),
|
||||||
|
newPsCommand(dockerCli),
|
||||||
newRemoveCommand(dockerCli),
|
newRemoveCommand(dockerCli),
|
||||||
newServicesCommand(dockerCli),
|
newServicesCommand(dockerCli),
|
||||||
newPsCommand(dockerCli),
|
|
||||||
)
|
)
|
||||||
|
flags := cmd.PersistentFlags()
|
||||||
|
flags.String("namespace", "default", "Kubernetes namespace to use")
|
||||||
|
flags.SetAnnotation("namespace", "kubernetes", nil)
|
||||||
|
flags.SetAnnotation("namespace", "experimentalCLI", nil)
|
||||||
|
flags.String("kubeconfig", "", "Kubernetes config file")
|
||||||
|
flags.SetAnnotation("kubeconfig", "kubernetes", nil)
|
||||||
|
flags.SetAnnotation("kubeconfig", "experimentalCLI", nil)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,36 +1,16 @@
|
||||||
package stack
|
package stack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/compose/convert"
|
"github.com/docker/cli/cli/command/stack/kubernetes"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
"github.com/docker/docker/api/types/versions"
|
"github.com/docker/cli/cli/command/stack/swarm"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/net/context"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
defaultNetworkDriver = "overlay"
|
|
||||||
resolveImageAlways = "always"
|
|
||||||
resolveImageChanged = "changed"
|
|
||||||
resolveImageNever = "never"
|
|
||||||
)
|
|
||||||
|
|
||||||
type deployOptions struct {
|
|
||||||
bundlefile string
|
|
||||||
composefile string
|
|
||||||
namespace string
|
|
||||||
resolveImage string
|
|
||||||
sendRegistryAuth bool
|
|
||||||
prune bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDeployCommand(dockerCli command.Cli) *cobra.Command {
|
func newDeployCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
var opts deployOptions
|
var opts options.Deploy
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "deploy [OPTIONS] STACK",
|
Use: "deploy [OPTIONS] STACK",
|
||||||
|
@ -38,85 +18,32 @@ func newDeployCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
Short: "Deploy a new stack or update an existing stack",
|
Short: "Deploy a new stack or update an existing stack",
|
||||||
Args: cli.ExactArgs(1),
|
Args: cli.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
opts.namespace = args[0]
|
opts.Namespace = args[0]
|
||||||
return runDeploy(dockerCli, opts)
|
if dockerCli.ClientInfo().HasKubernetes() {
|
||||||
|
kli, err := kubernetes.WrapCli(dockerCli, cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return kubernetes.RunDeploy(kli, opts)
|
||||||
|
}
|
||||||
|
return swarm.RunDeploy(dockerCli, opts)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
addBundlefileFlag(&opts.bundlefile, flags)
|
flags.StringVar(&opts.Bundlefile, "bundle-file", "", "Path to a Distributed Application Bundle file")
|
||||||
addComposefileFlag(&opts.composefile, flags)
|
flags.SetAnnotation("bundle-file", "experimental", nil)
|
||||||
addRegistryAuthFlag(&opts.sendRegistryAuth, flags)
|
flags.SetAnnotation("bundle-file", "swarm", nil)
|
||||||
flags.BoolVar(&opts.prune, "prune", false, "Prune services that are no longer referenced")
|
flags.StringVarP(&opts.Composefile, "compose-file", "c", "", "Path to a Compose file")
|
||||||
|
flags.SetAnnotation("compose-file", "version", []string{"1.25"})
|
||||||
|
flags.BoolVar(&opts.SendRegistryAuth, "with-registry-auth", false, "Send registry authentication details to Swarm agents")
|
||||||
|
flags.SetAnnotation("with-registry-auth", "swarm", nil)
|
||||||
|
flags.BoolVar(&opts.Prune, "prune", false, "Prune services that are no longer referenced")
|
||||||
flags.SetAnnotation("prune", "version", []string{"1.27"})
|
flags.SetAnnotation("prune", "version", []string{"1.27"})
|
||||||
flags.StringVar(&opts.resolveImage, "resolve-image", resolveImageAlways,
|
flags.SetAnnotation("prune", "swarm", nil)
|
||||||
`Query the registry to resolve image digest and supported platforms ("`+resolveImageAlways+`"|"`+resolveImageChanged+`"|"`+resolveImageNever+`")`)
|
flags.StringVar(&opts.ResolveImage, "resolve-image", swarm.ResolveImageAlways,
|
||||||
|
`Query the registry to resolve image digest and supported platforms ("`+swarm.ResolveImageAlways+`"|"`+swarm.ResolveImageChanged+`"|"`+swarm.ResolveImageNever+`")`)
|
||||||
flags.SetAnnotation("resolve-image", "version", []string{"1.30"})
|
flags.SetAnnotation("resolve-image", "version", []string{"1.30"})
|
||||||
|
flags.SetAnnotation("resolve-image", "swarm", nil)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDeploy(dockerCli command.Cli, opts deployOptions) error {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
if err := validateResolveImageFlag(dockerCli, &opts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case opts.bundlefile == "" && opts.composefile == "":
|
|
||||||
return errors.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).")
|
|
||||||
case opts.bundlefile != "" && opts.composefile != "":
|
|
||||||
return errors.Errorf("You cannot specify both a bundle file and a Compose file.")
|
|
||||||
case opts.bundlefile != "":
|
|
||||||
return deployBundle(ctx, dockerCli, opts)
|
|
||||||
default:
|
|
||||||
return deployCompose(ctx, dockerCli, opts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateResolveImageFlag validates the opts.resolveImage command line option
|
|
||||||
// and also turns image resolution off if the version is older than 1.30
|
|
||||||
func validateResolveImageFlag(dockerCli command.Cli, opts *deployOptions) error {
|
|
||||||
if opts.resolveImage != resolveImageAlways && opts.resolveImage != resolveImageChanged && opts.resolveImage != resolveImageNever {
|
|
||||||
return errors.Errorf("Invalid option %s for flag --resolve-image", opts.resolveImage)
|
|
||||||
}
|
|
||||||
// client side image resolution should not be done when the supported
|
|
||||||
// server version is older than 1.30
|
|
||||||
if versions.LessThan(dockerCli.Client().ClientVersion(), "1.30") {
|
|
||||||
opts.resolveImage = resolveImageNever
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkDaemonIsSwarmManager does an Info API call to verify that the daemon is
|
|
||||||
// a swarm manager. This is necessary because we must create networks before we
|
|
||||||
// create services, but the API call for creating a network does not return a
|
|
||||||
// proper status code when it can't create a network in the "global" scope.
|
|
||||||
func checkDaemonIsSwarmManager(ctx context.Context, dockerCli command.Cli) error {
|
|
||||||
info, err := dockerCli.Client().Info(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !info.Swarm.ControlAvailable {
|
|
||||||
return errors.New("this node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// pruneServices removes services that are no longer referenced in the source
|
|
||||||
func pruneServices(ctx context.Context, dockerCli command.Cli, namespace convert.Namespace, services map[string]struct{}) {
|
|
||||||
client := dockerCli.Client()
|
|
||||||
|
|
||||||
oldServices, err := getServices(ctx, client, namespace.Name())
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(dockerCli.Err(), "Failed to list services: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pruneServices := []swarm.Service{}
|
|
||||||
for _, service := range oldServices {
|
|
||||||
if _, exists := services[namespace.Descope(service.Spec.Name)]; !exists {
|
|
||||||
pruneServices = append(pruneServices, service)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
removeServices(ctx, dockerCli, pruneServices)
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// APIPresent checks that an API is installed.
|
||||||
|
func APIPresent(config *rest.Config) error {
|
||||||
|
log.Debugf("check API present at %s", config.Host)
|
||||||
|
clients, err := kubernetes.NewForConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, err := clients.Discovery().ServerGroups()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range groups.Groups {
|
||||||
|
if group.Name == apiv1beta1.SchemeGroupVersion.Group {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("could not find %s api. Install it on your cluster first", apiv1beta1.SchemeGroupVersion.Group)
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
|
composev1beta1 "github.com/docker/cli/kubernetes/client/clientset_generated/clientset/typed/compose/v1beta1"
|
||||||
|
"github.com/docker/docker/pkg/homedir"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
restclient "k8s.io/client-go/rest"
|
||||||
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KubeCli holds kubernetes specifics (client, namespace) with the command.Cli
|
||||||
|
type KubeCli struct {
|
||||||
|
command.Cli
|
||||||
|
kubeConfig *restclient.Config
|
||||||
|
kubeNamespace string
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapCli wraps command.Cli with kubernetes specifics
|
||||||
|
func WrapCli(dockerCli command.Cli, cmd *cobra.Command) (*KubeCli, error) {
|
||||||
|
var err error
|
||||||
|
cli := &KubeCli{
|
||||||
|
Cli: dockerCli,
|
||||||
|
kubeNamespace: "default",
|
||||||
|
}
|
||||||
|
if cmd.PersistentFlags().Changed("namespace") {
|
||||||
|
cli.kubeNamespace, err = cmd.PersistentFlags().GetString("namespace")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kubeConfig := ""
|
||||||
|
if cmd.PersistentFlags().Changed("kubeconfig") {
|
||||||
|
kubeConfig, err = cmd.PersistentFlags().GetString("kubeconfig")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if kubeConfig == "" {
|
||||||
|
if config := os.Getenv("KUBECONFIG"); config != "" {
|
||||||
|
kubeConfig = config
|
||||||
|
} else {
|
||||||
|
kubeConfig = filepath.Join(homedir.Get(), ".kube/config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := clientcmd.BuildConfigFromFlags("", kubeConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to load kubernetes configuration file '%s'", kubeConfig)
|
||||||
|
}
|
||||||
|
cli.kubeConfig = config
|
||||||
|
|
||||||
|
return cli, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *KubeCli) composeClient() (*Factory, error) {
|
||||||
|
return NewFactory(c.kubeNamespace, c.kubeConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *KubeCli) stacks() (composev1beta1.StackInterface, error) {
|
||||||
|
err := APIPresent(c.kubeConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientSet, err := composev1beta1.NewForConfig(c.kubeConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientSet.Stacks(c.kubeNamespace), nil
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
appsv1beta2 "k8s.io/client-go/kubernetes/typed/apps/v1beta2"
|
||||||
|
typesappsv1beta2 "k8s.io/client-go/kubernetes/typed/apps/v1beta2"
|
||||||
|
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
restclient "k8s.io/client-go/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Factory is the kubernetes client factory
|
||||||
|
type Factory struct {
|
||||||
|
namespace string
|
||||||
|
config *restclient.Config
|
||||||
|
coreClientSet *corev1.CoreV1Client
|
||||||
|
appsClientSet *appsv1beta2.AppsV1beta2Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFactory creates a kubernetes client factory
|
||||||
|
func NewFactory(namespace string, config *restclient.Config) (*Factory, error) {
|
||||||
|
coreClientSet, err := corev1.NewForConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
appsClientSet, err := appsv1beta2.NewForConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Factory{
|
||||||
|
namespace: namespace,
|
||||||
|
config: config,
|
||||||
|
coreClientSet: coreClientSet,
|
||||||
|
appsClientSet: appsClientSet,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigMaps returns a client for kubernetes's config maps
|
||||||
|
func (s *Factory) ConfigMaps() corev1.ConfigMapInterface {
|
||||||
|
return s.coreClientSet.ConfigMaps(s.namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secrets returns a client for kubernetes's secrets
|
||||||
|
func (s *Factory) Secrets() corev1.SecretInterface {
|
||||||
|
return s.coreClientSet.Secrets(s.namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Services returns a client for kubernetes's secrets
|
||||||
|
func (s *Factory) Services() corev1.ServiceInterface {
|
||||||
|
return s.coreClientSet.Services(s.namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pods returns a client for kubernetes's pods
|
||||||
|
func (s *Factory) Pods() corev1.PodInterface {
|
||||||
|
return s.coreClientSet.Pods(s.namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nodes returns a client for kubernetes's nodes
|
||||||
|
func (s *Factory) Nodes() corev1.NodeInterface {
|
||||||
|
return s.coreClientSet.Nodes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplicationControllers returns a client for kubernetes replication controllers
|
||||||
|
func (s *Factory) ReplicationControllers() corev1.ReplicationControllerInterface {
|
||||||
|
return s.coreClientSet.ReplicationControllers(s.namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplicaSets return a client for kubernetes replace sets
|
||||||
|
func (s *Factory) ReplicaSets() typesappsv1beta2.ReplicaSetInterface {
|
||||||
|
return s.appsClientSet.ReplicaSets(s.namespace)
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
|
apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
|
||||||
|
"github.com/docker/cli/kubernetes/labels"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsColliding verify that services defined in the stack collides with already deployed services
|
||||||
|
func IsColliding(services corev1.ServiceInterface, stack *apiv1beta1.Stack, cfg *composetypes.Config) error {
|
||||||
|
stackObjects := getServices(cfg)
|
||||||
|
|
||||||
|
for _, srv := range stackObjects {
|
||||||
|
if err := verify(services, stack.Name, srv); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify checks wether the service is already present in kubernetes.
|
||||||
|
// If we find the service by name but it doesn't have our label or it has a different value
|
||||||
|
// than the stack name for the label, we fail (i.e. it will collide)
|
||||||
|
func verify(services corev1.ServiceInterface, stackName string, service string) error {
|
||||||
|
svc, err := services.Get(service, metav1.GetOptions{})
|
||||||
|
if err == nil {
|
||||||
|
if key, ok := svc.ObjectMeta.Labels[labels.ForStackName]; ok {
|
||||||
|
if key != stackName {
|
||||||
|
return fmt.Errorf("service %s already present in stack named %s", service, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("service %s already present in the cluster", service)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServices(cfg *composetypes.Config) []string {
|
||||||
|
services := make([]string, len(cfg.Services))
|
||||||
|
for i := range cfg.Services {
|
||||||
|
services[i] = cfg.Services[i].Name
|
||||||
|
}
|
||||||
|
sort.Strings(services)
|
||||||
|
return services
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
|
"github.com/docker/cli/kubernetes/labels"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
appsv1beta2 "k8s.io/api/apps/v1beta2"
|
||||||
|
apiv1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pod conversion
|
||||||
|
func podToTask(pod apiv1.Pod) swarm.Task {
|
||||||
|
var startTime time.Time
|
||||||
|
if pod.Status.StartTime != nil {
|
||||||
|
startTime = (*pod.Status.StartTime).Time
|
||||||
|
}
|
||||||
|
task := swarm.Task{
|
||||||
|
ID: string(pod.UID),
|
||||||
|
NodeID: pod.Spec.NodeName,
|
||||||
|
Spec: swarm.TaskSpec{
|
||||||
|
ContainerSpec: &swarm.ContainerSpec{
|
||||||
|
Image: getContainerImage(pod.Spec.Containers),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DesiredState: podPhaseToState(pod.Status.Phase),
|
||||||
|
Status: swarm.TaskStatus{
|
||||||
|
State: podPhaseToState(pod.Status.Phase),
|
||||||
|
Timestamp: startTime,
|
||||||
|
PortStatus: swarm.PortStatus{
|
||||||
|
Ports: getPorts(pod.Spec.Containers),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
|
||||||
|
func podPhaseToState(phase apiv1.PodPhase) swarm.TaskState {
|
||||||
|
switch phase {
|
||||||
|
case apiv1.PodPending:
|
||||||
|
return swarm.TaskStatePending
|
||||||
|
case apiv1.PodRunning:
|
||||||
|
return swarm.TaskStateRunning
|
||||||
|
case apiv1.PodSucceeded:
|
||||||
|
return swarm.TaskStateComplete
|
||||||
|
case apiv1.PodFailed:
|
||||||
|
return swarm.TaskStateFailed
|
||||||
|
default:
|
||||||
|
return swarm.TaskState("unknown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSwarmProtocol(protocol apiv1.Protocol) swarm.PortConfigProtocol {
|
||||||
|
switch protocol {
|
||||||
|
case apiv1.ProtocolTCP:
|
||||||
|
return swarm.PortConfigProtocolTCP
|
||||||
|
case apiv1.ProtocolUDP:
|
||||||
|
return swarm.PortConfigProtocolUDP
|
||||||
|
}
|
||||||
|
return swarm.PortConfigProtocol("unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchPods(namespace string, pods corev1.PodInterface) ([]apiv1.Pod, error) {
|
||||||
|
labelSelector := labels.SelectorForStack(namespace)
|
||||||
|
podsList, err := pods.List(metav1.ListOptions{LabelSelector: labelSelector})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return podsList.Items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getContainerImage(containers []apiv1.Container) string {
|
||||||
|
if len(containers) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return containers[0].Image
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPorts(containers []apiv1.Container) []swarm.PortConfig {
|
||||||
|
if len(containers) == 0 || len(containers[0].Ports) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ports := make([]swarm.PortConfig, len(containers[0].Ports))
|
||||||
|
for i, port := range containers[0].Ports {
|
||||||
|
ports[i] = swarm.PortConfig{
|
||||||
|
PublishedPort: uint32(port.HostPort),
|
||||||
|
TargetPort: uint32(port.ContainerPort),
|
||||||
|
Protocol: toSwarmProtocol(port.Protocol),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ports
|
||||||
|
}
|
||||||
|
|
||||||
|
type tasksBySlot []swarm.Task
|
||||||
|
|
||||||
|
func (t tasksBySlot) Len() int {
|
||||||
|
return len(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t tasksBySlot) Swap(i, j int) {
|
||||||
|
t[i], t[j] = t[j], t[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t tasksBySlot) Less(i, j int) bool {
|
||||||
|
// Sort by slot.
|
||||||
|
if t[i].Slot != t[j].Slot {
|
||||||
|
return t[i].Slot < t[j].Slot
|
||||||
|
}
|
||||||
|
|
||||||
|
// If same slot, sort by most recent.
|
||||||
|
return t[j].Meta.CreatedAt.Before(t[i].CreatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replicas conversion
|
||||||
|
func replicasToServices(replicas *appsv1beta2.ReplicaSetList, services *apiv1.ServiceList) ([]swarm.Service, map[string]formatter.ServiceListInfo, error) {
|
||||||
|
result := make([]swarm.Service, len(replicas.Items))
|
||||||
|
infos := make(map[string]formatter.ServiceListInfo, len(replicas.Items))
|
||||||
|
for i, r := range replicas.Items {
|
||||||
|
service, ok := findService(services, r.Labels[labels.ForServiceName])
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, fmt.Errorf("could not find service '%s'", r.Labels[labels.ForServiceName])
|
||||||
|
}
|
||||||
|
stack, ok := service.Labels[labels.ForStackName]
|
||||||
|
if ok {
|
||||||
|
stack += "_"
|
||||||
|
}
|
||||||
|
uid := string(service.UID)
|
||||||
|
s := swarm.Service{
|
||||||
|
ID: uid,
|
||||||
|
Spec: swarm.ServiceSpec{
|
||||||
|
Annotations: swarm.Annotations{
|
||||||
|
Name: stack + service.Name,
|
||||||
|
},
|
||||||
|
TaskTemplate: swarm.TaskSpec{
|
||||||
|
ContainerSpec: &swarm.ContainerSpec{
|
||||||
|
Image: getContainerImage(r.Spec.Template.Spec.Containers),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if service.Spec.Type == apiv1.ServiceTypeLoadBalancer {
|
||||||
|
configs := make([]swarm.PortConfig, len(service.Spec.Ports))
|
||||||
|
for i, p := range service.Spec.Ports {
|
||||||
|
configs[i] = swarm.PortConfig{
|
||||||
|
PublishMode: swarm.PortConfigPublishModeIngress,
|
||||||
|
PublishedPort: uint32(p.Port),
|
||||||
|
TargetPort: uint32(p.TargetPort.IntValue()),
|
||||||
|
Protocol: toSwarmProtocol(p.Protocol),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.Endpoint = swarm.Endpoint{Ports: configs}
|
||||||
|
}
|
||||||
|
result[i] = s
|
||||||
|
infos[uid] = formatter.ServiceListInfo{
|
||||||
|
Mode: "replicated",
|
||||||
|
Replicas: fmt.Sprintf("%d/%d", r.Status.AvailableReplicas, r.Status.Replicas),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, infos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findService(services *apiv1.ServiceList, name string) (apiv1.Service, bool) {
|
||||||
|
for _, s := range services.Items {
|
||||||
|
if s.Name == name {
|
||||||
|
return s, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return apiv1.Service{}, false
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/cli/kubernetes/labels"
|
||||||
|
apiv1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// toConfigMap converts a Compose Config to a Kube ConfigMap.
|
||||||
|
func toConfigMap(stackName, name, key string, content []byte) *apiv1.ConfigMap {
|
||||||
|
return &apiv1.ConfigMap{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "ConfigMap",
|
||||||
|
APIVersion: "v1",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Labels: map[string]string{
|
||||||
|
labels.ForStackName: stackName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Data: map[string]string{
|
||||||
|
key: string(content),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// toSecret converts a Compose Secret to a Kube Secret.
|
||||||
|
func toSecret(stackName, name, key string, content []byte) *apiv1.Secret {
|
||||||
|
return &apiv1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Labels: map[string]string{
|
||||||
|
labels.ForStackName: stackName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
key: content,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
|
composeTypes "github.com/docker/cli/cli/compose/types"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunDeploy is the kubernetes implementation of docker stack deploy
|
||||||
|
func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error {
|
||||||
|
cmdOut := dockerCli.Out()
|
||||||
|
// Check arguments
|
||||||
|
if opts.Composefile == "" {
|
||||||
|
return errors.Errorf("Please specify a Compose file (with --compose-file).")
|
||||||
|
}
|
||||||
|
// Initialize clients
|
||||||
|
stacks, err := dockerCli.stacks()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
composeClient, err := dockerCli.composeClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
configMaps := composeClient.ConfigMaps()
|
||||||
|
secrets := composeClient.Secrets()
|
||||||
|
services := composeClient.Services()
|
||||||
|
pods := composeClient.Pods()
|
||||||
|
watcher := DeployWatcher{
|
||||||
|
Pods: pods,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the compose file
|
||||||
|
stack, cfg, err := LoadStack(opts.Namespace, opts.Composefile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME(vdemeester) handle warnings server-side
|
||||||
|
if err = IsColliding(services, stack, cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = createFileBasedConfigMaps(stack.Name, cfg.Configs, configMaps); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = createFileBasedSecrets(stack.Name, cfg.Secrets, secrets); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if in, err := stacks.Get(stack.Name, metav1.GetOptions{}); err == nil {
|
||||||
|
in.Spec = stack.Spec
|
||||||
|
|
||||||
|
if _, err = stacks.Update(in); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Stack %s was updated\n", stack.Name)
|
||||||
|
} else {
|
||||||
|
if _, err = stacks.Create(stack); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(cmdOut, "Stack %s was created\n", stack.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(cmdOut, "Waiting for the stack to be stable and running...")
|
||||||
|
|
||||||
|
<-watcher.Watch(stack, serviceNames(cfg))
|
||||||
|
|
||||||
|
fmt.Fprintf(cmdOut, "Stack %s is stable and running\n\n", stack.Name)
|
||||||
|
// TODO: fmt.Fprintf(cmdOut, "Read the logs with:\n $ %s stack logs %s\n", filepath.Base(os.Args[0]), stack.Name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createFileBasedConfigMaps creates a Kubernetes ConfigMap for each Compose global file-based config.
|
||||||
|
func createFileBasedConfigMaps(stackName string, globalConfigs map[string]composeTypes.ConfigObjConfig, configMaps corev1.ConfigMapInterface) error {
|
||||||
|
for name, config := range globalConfigs {
|
||||||
|
if config.File == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := path.Base(config.File)
|
||||||
|
content, err := ioutil.ReadFile(config.File)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := configMaps.Create(toConfigMap(stackName, name, fileName, content)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func serviceNames(cfg *composeTypes.Config) []string {
|
||||||
|
names := []string{}
|
||||||
|
|
||||||
|
for _, service := range cfg.Services {
|
||||||
|
names = append(names, service.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// createFileBasedSecrets creates a Kubernetes Secret for each Compose global file-based secret.
|
||||||
|
func createFileBasedSecrets(stackName string, globalSecrets map[string]composeTypes.SecretConfig, secrets corev1.SecretInterface) error {
|
||||||
|
for name, secret := range globalSecrets {
|
||||||
|
if secret.File == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := path.Base(secret.File)
|
||||||
|
content, err := ioutil.ReadFile(secret.File)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := secrets.Create(toSecret(stackName, name, fileName, content)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
|
"github.com/docker/cli/cli/compose/loader"
|
||||||
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"vbom.ml/util/sortorder"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunList is the kubernetes implementation of docker stack ls
|
||||||
|
func RunList(dockerCli *KubeCli, opts options.List) error {
|
||||||
|
stacks, err := getStacks(dockerCli)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
format := opts.Format
|
||||||
|
if len(format) == 0 {
|
||||||
|
format = formatter.TableFormatKey
|
||||||
|
}
|
||||||
|
stackCtx := formatter.Context{
|
||||||
|
Output: dockerCli.Out(),
|
||||||
|
Format: formatter.NewStackFormat(format),
|
||||||
|
}
|
||||||
|
sort.Sort(byName(stacks))
|
||||||
|
return formatter.StackWrite(stackCtx, stacks)
|
||||||
|
}
|
||||||
|
|
||||||
|
type byName []*formatter.Stack
|
||||||
|
|
||||||
|
func (n byName) Len() int { return len(n) }
|
||||||
|
func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
|
||||||
|
func (n byName) Less(i, j int) bool { return sortorder.NaturalLess(n[i].Name, n[j].Name) }
|
||||||
|
|
||||||
|
func getStacks(kubeCli *KubeCli) ([]*formatter.Stack, error) {
|
||||||
|
stackSvc, err := kubeCli.stacks()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stacks, err := stackSvc.List(metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var formattedStacks []*formatter.Stack
|
||||||
|
for _, stack := range stacks.Items {
|
||||||
|
cfg, err := loadStack(stack.Spec.ComposeFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
formattedStacks = append(formattedStacks, &formatter.Stack{
|
||||||
|
Name: stack.Name,
|
||||||
|
Services: len(getServices(cfg)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return formattedStacks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadStack(composefile string) (*composetypes.Config, error) {
|
||||||
|
parsed, err := loader.ParseYAML([]byte(composefile))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return loader.Load(composetypes.ConfigDetails{
|
||||||
|
ConfigFiles: []composetypes.ConfigFile{
|
||||||
|
{
|
||||||
|
Config: parsed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,169 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/compose/loader"
|
||||||
|
"github.com/docker/cli/cli/compose/template"
|
||||||
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
|
apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadStack loads a stack from a Compose file, with a given name.
|
||||||
|
// FIXME(vdemeester) remove this and use cli/compose/loader for both swarm and kubernetes
|
||||||
|
func LoadStack(name, composeFile string) (*apiv1beta1.Stack, *composetypes.Config, error) {
|
||||||
|
if composeFile == "" {
|
||||||
|
return nil, nil, errors.New("compose-file must be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
workingDir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
composePath := composeFile
|
||||||
|
if !strings.HasPrefix(composePath, "/") {
|
||||||
|
composePath = filepath.Join(workingDir, composeFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(composePath); os.IsNotExist(err) {
|
||||||
|
return nil, nil, errors.Errorf("no compose file found in %s", filepath.Dir(composePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
binary, err := ioutil.ReadFile(composePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "cannot read compose file")
|
||||||
|
}
|
||||||
|
|
||||||
|
env := env(workingDir)
|
||||||
|
return load(name, binary, workingDir, env)
|
||||||
|
}
|
||||||
|
|
||||||
|
func load(name string, binary []byte, workingDir string, env map[string]string) (*apiv1beta1.Stack, *composetypes.Config, error) {
|
||||||
|
processed, err := template.Substitute(string(binary), func(key string) (string, bool) { return env[key], true })
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "cannot load compose file")
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := loader.ParseYAML([]byte(processed))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrapf(err, "cannot load compose file")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := loader.Load(composetypes.ConfigDetails{
|
||||||
|
WorkingDir: workingDir,
|
||||||
|
ConfigFiles: []composetypes.ConfigFile{
|
||||||
|
{
|
||||||
|
Config: parsed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrapf(err, "cannot load compose file")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := processEnvFiles(processed, parsed, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrapf(err, "cannot load compose file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &apiv1beta1.Stack{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
Spec: apiv1beta1.StackSpec{
|
||||||
|
ComposeFile: result,
|
||||||
|
},
|
||||||
|
}, cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type iMap = map[string]interface{}
|
||||||
|
|
||||||
|
func processEnvFiles(input string, parsed map[string]interface{}, config *composetypes.Config) (string, error) {
|
||||||
|
changed := false
|
||||||
|
|
||||||
|
for _, svc := range config.Services {
|
||||||
|
if len(svc.EnvFile) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Load() processed the env_file for us, we just need to inject back into
|
||||||
|
// the intermediate representation
|
||||||
|
env := iMap{}
|
||||||
|
for k, v := range svc.Environment {
|
||||||
|
env[k] = v
|
||||||
|
}
|
||||||
|
parsed["services"].(iMap)[svc.Name].(iMap)["environment"] = env
|
||||||
|
delete(parsed["services"].(iMap)[svc.Name].(iMap), "env_file")
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if !changed {
|
||||||
|
return input, nil
|
||||||
|
}
|
||||||
|
res, err := yaml.Marshal(parsed)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(res), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func env(workingDir string) map[string]string {
|
||||||
|
// Apply .env file first
|
||||||
|
config := readEnvFile(filepath.Join(workingDir, ".env"))
|
||||||
|
|
||||||
|
// Apply env variables
|
||||||
|
for k, v := range envToMap(os.Environ()) {
|
||||||
|
config[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func readEnvFile(path string) map[string]string {
|
||||||
|
config := map[string]string{}
|
||||||
|
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return config // Ignore
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if strings.HasPrefix(strings.TrimSpace(line), "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
key := parts[0]
|
||||||
|
value := parts[1]
|
||||||
|
|
||||||
|
config[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func envToMap(env []string) map[string]string {
|
||||||
|
config := map[string]string{}
|
||||||
|
|
||||||
|
for _, value := range env {
|
||||||
|
parts := strings.SplitN(value, "=", 2)
|
||||||
|
|
||||||
|
key := parts[0]
|
||||||
|
value := parts[1]
|
||||||
|
|
||||||
|
config[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPlaceholders(t *testing.T) {
|
||||||
|
env := map[string]string{
|
||||||
|
"TAG": "_latest_",
|
||||||
|
"K1": "V1",
|
||||||
|
"K2": "V2",
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := "version: '3'\nvolumes:\n data:\n external:\n name: "
|
||||||
|
var tests = []struct {
|
||||||
|
input string
|
||||||
|
expectedOutput string
|
||||||
|
}{
|
||||||
|
{prefix + "BEFORE${TAG}AFTER", prefix + "BEFORE_latest_AFTER"},
|
||||||
|
{prefix + "BEFORE${K1}${K2}AFTER", prefix + "BEFOREV1V2AFTER"},
|
||||||
|
{prefix + "BEFORE$TAG AFTER", prefix + "BEFORE_latest_ AFTER"},
|
||||||
|
{prefix + "BEFORE$$TAG AFTER", prefix + "BEFORE$TAG AFTER"},
|
||||||
|
{prefix + "BEFORE $UNKNOWN AFTER", prefix + "BEFORE AFTER"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
output, _, err := load("stack", []byte(test.input), ".", env)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expectedOutput, output.Spec.ComposeFile)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
|
"github.com/docker/cli/cli/command/task"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
apiv1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
"k8s.io/kubernetes/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunPS is the kubernetes implementation of docker stack ps
|
||||||
|
func RunPS(dockerCli *KubeCli, options options.PS) error {
|
||||||
|
namespace := options.Namespace
|
||||||
|
|
||||||
|
// Initialize clients
|
||||||
|
client, err := dockerCli.composeClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stacks, err := dockerCli.stacks()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
podsClient := client.Pods()
|
||||||
|
|
||||||
|
// Fetch pods
|
||||||
|
if _, err := stacks.Get(namespace, metav1.GetOptions{}); err != nil {
|
||||||
|
return fmt.Errorf("nothing found in stack: %s", namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
pods, err := fetchPods(namespace, podsClient)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pods) == 0 {
|
||||||
|
return fmt.Errorf("nothing found in stack: %s", namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
format := options.Format
|
||||||
|
if len(format) == 0 {
|
||||||
|
format = task.DefaultFormat(dockerCli.ConfigFile(), options.Quiet)
|
||||||
|
}
|
||||||
|
nodeResolver := makeNodeResolver(options.NoResolve, client.Nodes())
|
||||||
|
|
||||||
|
tasks := make([]swarm.Task, len(pods))
|
||||||
|
for i, pod := range pods {
|
||||||
|
tasks[i] = podToTask(pod)
|
||||||
|
}
|
||||||
|
return print(dockerCli, namespace, tasks, pods, nodeResolver, !options.NoTrunc, options.Quiet, format)
|
||||||
|
}
|
||||||
|
|
||||||
|
type idResolver func(name string) (string, error)
|
||||||
|
|
||||||
|
func print(dockerCli command.Cli, namespace string, tasks []swarm.Task, pods []apiv1.Pod, nodeResolver idResolver, trunc, quiet bool, format string) error {
|
||||||
|
sort.Stable(tasksBySlot(tasks))
|
||||||
|
|
||||||
|
names := map[string]string{}
|
||||||
|
nodes := map[string]string{}
|
||||||
|
|
||||||
|
tasksCtx := formatter.Context{
|
||||||
|
Output: dockerCli.Out(),
|
||||||
|
Format: formatter.NewTaskFormat(format, quiet),
|
||||||
|
Trunc: trunc,
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, task := range tasks {
|
||||||
|
nodeValue, err := nodeResolver(pods[i].Spec.NodeName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
names[task.ID] = fmt.Sprintf("%s_%s", namespace, pods[i].Name)
|
||||||
|
nodes[task.ID] = nodeValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatter.TaskWrite(tasksCtx, tasks, names, nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeNodeResolver(noResolve bool, nodes corev1.NodeInterface) func(string) (string, error) {
|
||||||
|
// Here we have a name and we need to resolve its identifier. To mimic swarm behavior
|
||||||
|
// we need to resolve the id when noresolve is set, otherwise we return the name.
|
||||||
|
if noResolve {
|
||||||
|
return func(name string) (string, error) {
|
||||||
|
n, err := nodes.List(metav1.ListOptions{
|
||||||
|
FieldSelector: fields.OneTermEqualSelector(api.ObjectNameField, name).String(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(n.Items) != 1 {
|
||||||
|
return "", fmt.Errorf("could not find node '%s'", name)
|
||||||
|
}
|
||||||
|
return string(n.Items[0].UID), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return func(name string) (string, error) { return name, nil }
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunRemove is the kubernetes implementation of docker stack remove
|
||||||
|
func RunRemove(dockerCli *KubeCli, opts options.Remove) error {
|
||||||
|
stacks, err := dockerCli.stacks()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, stack := range opts.Namespaces {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "Removing stack: %s\n", stack)
|
||||||
|
err := stacks.Delete(stack, &metav1.DeleteOptions{})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "Failed to remove stack %s: %s\n", stack, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
|
"github.com/docker/cli/kubernetes/labels"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunServices is the kubernetes implementation of docker stack services
|
||||||
|
func RunServices(dockerCli *KubeCli, opts options.Services) error {
|
||||||
|
// Initialize clients
|
||||||
|
client, err := dockerCli.composeClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
stacks, err := dockerCli.stacks()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
replicas := client.ReplicaSets()
|
||||||
|
|
||||||
|
if _, err := stacks.Get(opts.Namespace, metav1.GetOptions{}); err != nil {
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", opts.Namespace)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
replicasList, err := replicas.List(metav1.ListOptions{LabelSelector: labels.SelectorForStack(opts.Namespace)})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
servicesList, err := client.Services().List(metav1.ListOptions{LabelSelector: labels.SelectorForStack(opts.Namespace)})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Replicas sets and kubernetes services to swam services and formatter informations
|
||||||
|
services, info, err := replicasToServices(replicasList, servicesList)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Quiet {
|
||||||
|
info = map[string]formatter.ServiceListInfo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
format := opts.Format
|
||||||
|
if len(format) == 0 {
|
||||||
|
if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.Quiet {
|
||||||
|
format = dockerCli.ConfigFile().ServicesFormat
|
||||||
|
} else {
|
||||||
|
format = formatter.TableFormatKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
servicesCtx := formatter.Context{
|
||||||
|
Output: dockerCli.Out(),
|
||||||
|
Format: formatter.NewServiceListFormat(format, opts.Quiet),
|
||||||
|
}
|
||||||
|
return formatter.ServiceListWrite(servicesCtx, services, info)
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
package kubernetes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
apiv1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
|
||||||
|
"github.com/docker/cli/kubernetes/labels"
|
||||||
|
apiv1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeployWatcher watches a stack deployement
|
||||||
|
type DeployWatcher struct {
|
||||||
|
Pods corev1.PodInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch watches a stuck deployement and return a chan that will holds the state of the stack
|
||||||
|
func (w DeployWatcher) Watch(stack *apiv1beta1.Stack, serviceNames []string) chan bool {
|
||||||
|
stop := make(chan bool)
|
||||||
|
|
||||||
|
go w.waitForPods(stack.Name, serviceNames, stop)
|
||||||
|
|
||||||
|
return stop
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w DeployWatcher) waitForPods(stackName string, serviceNames []string, stop chan bool) {
|
||||||
|
starts := map[string]int32{}
|
||||||
|
|
||||||
|
for {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
list, err := w.Pods.List(metav1.ListOptions{
|
||||||
|
LabelSelector: labels.SelectorForStack(stackName),
|
||||||
|
IncludeUninitialized: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
stop <- true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range list.Items {
|
||||||
|
pod := list.Items[i]
|
||||||
|
if pod.Status.Phase != apiv1.PodRunning {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
startCount := startCount(pod)
|
||||||
|
serviceName := pod.Labels[labels.ForServiceName]
|
||||||
|
if startCount != starts[serviceName] {
|
||||||
|
if startCount == 1 {
|
||||||
|
fmt.Printf(" - Service %s has one container running\n", serviceName)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" - Service %s was restarted %d %s\n", serviceName, startCount-1, timeTimes(startCount-1))
|
||||||
|
}
|
||||||
|
|
||||||
|
starts[serviceName] = startCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allReady(list.Items, serviceNames) {
|
||||||
|
stop <- true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startCount(pod apiv1.Pod) int32 {
|
||||||
|
restart := int32(0)
|
||||||
|
|
||||||
|
for _, status := range pod.Status.ContainerStatuses {
|
||||||
|
restart += status.RestartCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1 + restart
|
||||||
|
}
|
||||||
|
|
||||||
|
func allReady(pods []apiv1.Pod, serviceNames []string) bool {
|
||||||
|
serviceUp := map[string]bool{}
|
||||||
|
|
||||||
|
for _, pod := range pods {
|
||||||
|
if time.Since(pod.GetCreationTimestamp().Time) < 10*time.Second {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
ready := false
|
||||||
|
for _, cond := range pod.Status.Conditions {
|
||||||
|
if cond.Type == apiv1.PodReady && cond.Status == apiv1.ConditionTrue {
|
||||||
|
ready = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ready {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceName := pod.Labels[labels.ForServiceName]
|
||||||
|
serviceUp[serviceName] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, serviceName := range serviceNames {
|
||||||
|
if !serviceUp[serviceName] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeTimes(n int32) string {
|
||||||
|
if n == 1 {
|
||||||
|
return "time"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "times"
|
||||||
|
}
|
|
@ -1,26 +1,16 @@
|
||||||
package stack
|
package stack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/formatter"
|
"github.com/docker/cli/cli/command/stack/kubernetes"
|
||||||
"github.com/docker/cli/cli/compose/convert"
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/cli/cli/command/stack/swarm"
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/net/context"
|
|
||||||
"vbom.ml/util/sortorder"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type listOptions struct {
|
|
||||||
format string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newListCommand(dockerCli command.Cli) *cobra.Command {
|
func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
opts := listOptions{}
|
opts := options.List{}
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "ls",
|
Use: "ls",
|
||||||
|
@ -28,69 +18,18 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
Short: "List stacks",
|
Short: "List stacks",
|
||||||
Args: cli.NoArgs,
|
Args: cli.NoArgs,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runList(dockerCli, opts)
|
if dockerCli.ClientInfo().HasKubernetes() {
|
||||||
|
kli, err := kubernetes.WrapCli(dockerCli, cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return kubernetes.RunList(kli, opts)
|
||||||
|
}
|
||||||
|
return swarm.RunList(dockerCli, opts)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
flags.StringVar(&opts.format, "format", "", "Pretty-print stacks using a Go template")
|
flags.StringVar(&opts.Format, "format", "", "Pretty-print stacks using a Go template")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runList(dockerCli command.Cli, opts listOptions) error {
|
|
||||||
client := dockerCli.Client()
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
stacks, err := getStacks(ctx, client)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
format := opts.format
|
|
||||||
if len(format) == 0 {
|
|
||||||
format = formatter.TableFormatKey
|
|
||||||
}
|
|
||||||
stackCtx := formatter.Context{
|
|
||||||
Output: dockerCli.Out(),
|
|
||||||
Format: formatter.NewStackFormat(format),
|
|
||||||
}
|
|
||||||
sort.Sort(byName(stacks))
|
|
||||||
return formatter.StackWrite(stackCtx, stacks)
|
|
||||||
}
|
|
||||||
|
|
||||||
type byName []*formatter.Stack
|
|
||||||
|
|
||||||
func (n byName) Len() int { return len(n) }
|
|
||||||
func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
|
|
||||||
func (n byName) Less(i, j int) bool { return sortorder.NaturalLess(n[i].Name, n[j].Name) }
|
|
||||||
|
|
||||||
func getStacks(ctx context.Context, apiclient client.APIClient) ([]*formatter.Stack, error) {
|
|
||||||
services, err := apiclient.ServiceList(
|
|
||||||
ctx,
|
|
||||||
types.ServiceListOptions{Filters: getAllStacksFilter()})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
m := make(map[string]*formatter.Stack)
|
|
||||||
for _, service := range services {
|
|
||||||
labels := service.Spec.Labels
|
|
||||||
name, ok := labels[convert.LabelNamespace]
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.Errorf("cannot get label %s for service %s",
|
|
||||||
convert.LabelNamespace, service.ID)
|
|
||||||
}
|
|
||||||
ztack, ok := m[name]
|
|
||||||
if !ok {
|
|
||||||
m[name] = &formatter.Stack{
|
|
||||||
Name: name,
|
|
||||||
Services: 1,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ztack.Services++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var stacks []*formatter.Stack
|
|
||||||
for _, stack := range m {
|
|
||||||
stacks = append(stacks, stack)
|
|
||||||
}
|
|
||||||
return stacks, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
package options
|
||||||
|
|
||||||
|
import "github.com/docker/cli/opts"
|
||||||
|
|
||||||
|
// Deploy holds docker stack deploy options
|
||||||
|
type Deploy struct {
|
||||||
|
Bundlefile string
|
||||||
|
Composefile string
|
||||||
|
Namespace string
|
||||||
|
ResolveImage string
|
||||||
|
SendRegistryAuth bool
|
||||||
|
Prune bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// List holds docker stack ls options
|
||||||
|
type List struct {
|
||||||
|
Format string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PS holds docker stack ps options
|
||||||
|
type PS struct {
|
||||||
|
Filter opts.FilterOpt
|
||||||
|
NoTrunc bool
|
||||||
|
Namespace string
|
||||||
|
NoResolve bool
|
||||||
|
Quiet bool
|
||||||
|
Format string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove holds docker stack remove options
|
||||||
|
type Remove struct {
|
||||||
|
Namespaces []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Services holds docker stack services options
|
||||||
|
type Services struct {
|
||||||
|
Quiet bool
|
||||||
|
Format string
|
||||||
|
Filter opts.FilterOpt
|
||||||
|
Namespace string
|
||||||
|
}
|
|
@ -1,51 +0,0 @@
|
||||||
package stack
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command/bundlefile"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
)
|
|
||||||
|
|
||||||
func addComposefileFlag(opt *string, flags *pflag.FlagSet) {
|
|
||||||
flags.StringVarP(opt, "compose-file", "c", "", "Path to a Compose file")
|
|
||||||
flags.SetAnnotation("compose-file", "version", []string{"1.25"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func addBundlefileFlag(opt *string, flags *pflag.FlagSet) {
|
|
||||||
flags.StringVar(opt, "bundle-file", "", "Path to a Distributed Application Bundle file")
|
|
||||||
flags.SetAnnotation("bundle-file", "experimental", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func addRegistryAuthFlag(opt *bool, flags *pflag.FlagSet) {
|
|
||||||
flags.BoolVar(opt, "with-registry-auth", false, "Send registry authentication details to Swarm agents")
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefile.Bundlefile, error) {
|
|
||||||
defaultPath := fmt.Sprintf("%s.dab", namespace)
|
|
||||||
|
|
||||||
if path == "" {
|
|
||||||
path = defaultPath
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(path); err != nil {
|
|
||||||
return nil, errors.Errorf(
|
|
||||||
"Bundle %s not found. Specify the path with --file",
|
|
||||||
path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(stderr, "Loading bundle from %s\n", path)
|
|
||||||
reader, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer reader.Close()
|
|
||||||
|
|
||||||
bundle, err := bundlefile.LoadFile(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Errorf("Error reading %s: %v\n", path, err)
|
|
||||||
}
|
|
||||||
return bundle, err
|
|
||||||
}
|
|
|
@ -1,69 +1,41 @@
|
||||||
package stack
|
package stack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/idresolver"
|
"github.com/docker/cli/cli/command/stack/kubernetes"
|
||||||
"github.com/docker/cli/cli/command/task"
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/cli/command/stack/swarm"
|
||||||
"github.com/docker/docker/api/types"
|
cliopts "github.com/docker/cli/opts"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/net/context"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type psOptions struct {
|
|
||||||
filter opts.FilterOpt
|
|
||||||
noTrunc bool
|
|
||||||
namespace string
|
|
||||||
noResolve bool
|
|
||||||
quiet bool
|
|
||||||
format string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPsCommand(dockerCli command.Cli) *cobra.Command {
|
func newPsCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
options := psOptions{filter: opts.NewFilterOpt()}
|
opts := options.PS{Filter: cliopts.NewFilterOpt()}
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "ps [OPTIONS] STACK",
|
Use: "ps [OPTIONS] STACK",
|
||||||
Short: "List the tasks in the stack",
|
Short: "List the tasks in the stack",
|
||||||
Args: cli.ExactArgs(1),
|
Args: cli.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
options.namespace = args[0]
|
opts.Namespace = args[0]
|
||||||
return runPS(dockerCli, options)
|
if dockerCli.ClientInfo().HasKubernetes() {
|
||||||
|
kli, err := kubernetes.WrapCli(dockerCli, cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return kubernetes.RunPS(kli, opts)
|
||||||
|
}
|
||||||
|
return swarm.RunPS(dockerCli, opts)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
flags.BoolVar(&options.noTrunc, "no-trunc", false, "Do not truncate output")
|
flags.BoolVar(&opts.NoTrunc, "no-trunc", false, "Do not truncate output")
|
||||||
flags.BoolVar(&options.noResolve, "no-resolve", false, "Do not map IDs to Names")
|
flags.BoolVar(&opts.NoResolve, "no-resolve", false, "Do not map IDs to Names")
|
||||||
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
|
flags.VarP(&opts.Filter, "filter", "f", "Filter output based on conditions provided")
|
||||||
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display task IDs")
|
flags.SetAnnotation("filter", "swarm", nil)
|
||||||
flags.StringVar(&options.format, "format", "", "Pretty-print tasks using a Go template")
|
flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display task IDs")
|
||||||
|
flags.StringVar(&opts.Format, "format", "", "Pretty-print tasks using a Go template")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runPS(dockerCli command.Cli, options psOptions) error {
|
|
||||||
namespace := options.namespace
|
|
||||||
client := dockerCli.Client()
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
filter := getStackFilterFromOpt(options.namespace, options.filter)
|
|
||||||
|
|
||||||
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(tasks) == 0 {
|
|
||||||
return fmt.Errorf("nothing found in stack: %s", namespace)
|
|
||||||
}
|
|
||||||
|
|
||||||
format := options.format
|
|
||||||
if len(format) == 0 {
|
|
||||||
format = task.DefaultFormat(dockerCli.ConfigFile(), options.quiet)
|
|
||||||
}
|
|
||||||
|
|
||||||
return task.Print(ctx, dockerCli, tasks, idresolver.New(client, options.noResolve), !options.noTrunc, options.quiet, format)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,26 +1,16 @@
|
||||||
package stack
|
package stack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/cli/cli/command/stack/kubernetes"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
"github.com/docker/docker/api/types/versions"
|
"github.com/docker/cli/cli/command/stack/swarm"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/net/context"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type removeOptions struct {
|
|
||||||
namespaces []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
var opts removeOptions
|
var opts options.Remove
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "rm STACK [STACK...]",
|
Use: "rm STACK [STACK...]",
|
||||||
|
@ -28,134 +18,16 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
Short: "Remove one or more stacks",
|
Short: "Remove one or more stacks",
|
||||||
Args: cli.RequiresMinArgs(1),
|
Args: cli.RequiresMinArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
opts.namespaces = args
|
opts.Namespaces = args
|
||||||
return runRemove(dockerCli, opts)
|
if dockerCli.ClientInfo().HasKubernetes() {
|
||||||
|
kli, err := kubernetes.WrapCli(dockerCli, cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return kubernetes.RunRemove(kli, opts)
|
||||||
|
}
|
||||||
|
return swarm.RunRemove(dockerCli, opts)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runRemove(dockerCli command.Cli, opts removeOptions) error {
|
|
||||||
namespaces := opts.namespaces
|
|
||||||
client := dockerCli.Client()
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
var errs []string
|
|
||||||
for _, namespace := range namespaces {
|
|
||||||
services, err := getServices(ctx, client, namespace)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
networks, err := getStackNetworks(ctx, client, namespace)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var secrets []swarm.Secret
|
|
||||||
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.25") {
|
|
||||||
secrets, err = getStackSecrets(ctx, client, namespace)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var configs []swarm.Config
|
|
||||||
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.30") {
|
|
||||||
configs, err = getStackConfigs(ctx, client, namespace)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(services)+len(networks)+len(secrets)+len(configs) == 0 {
|
|
||||||
fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", namespace)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
hasError := removeServices(ctx, dockerCli, services)
|
|
||||||
hasError = removeSecrets(ctx, dockerCli, secrets) || hasError
|
|
||||||
hasError = removeConfigs(ctx, dockerCli, configs) || hasError
|
|
||||||
hasError = removeNetworks(ctx, dockerCli, networks) || hasError
|
|
||||||
|
|
||||||
if hasError {
|
|
||||||
errs = append(errs, fmt.Sprintf("Failed to remove some resources from stack: %s", namespace))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(errs) > 0 {
|
|
||||||
return errors.Errorf(strings.Join(errs, "\n"))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func sortServiceByName(services []swarm.Service) func(i, j int) bool {
|
|
||||||
return func(i, j int) bool {
|
|
||||||
return services[i].Spec.Name < services[j].Spec.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeServices(
|
|
||||||
ctx context.Context,
|
|
||||||
dockerCli command.Cli,
|
|
||||||
services []swarm.Service,
|
|
||||||
) bool {
|
|
||||||
var hasError bool
|
|
||||||
sort.Slice(services, sortServiceByName(services))
|
|
||||||
for _, service := range services {
|
|
||||||
fmt.Fprintf(dockerCli.Out(), "Removing service %s\n", service.Spec.Name)
|
|
||||||
if err := dockerCli.Client().ServiceRemove(ctx, service.ID); err != nil {
|
|
||||||
hasError = true
|
|
||||||
fmt.Fprintf(dockerCli.Err(), "Failed to remove service %s: %s", service.ID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hasError
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeNetworks(
|
|
||||||
ctx context.Context,
|
|
||||||
dockerCli command.Cli,
|
|
||||||
networks []types.NetworkResource,
|
|
||||||
) bool {
|
|
||||||
var hasError bool
|
|
||||||
for _, network := range networks {
|
|
||||||
fmt.Fprintf(dockerCli.Out(), "Removing network %s\n", network.Name)
|
|
||||||
if err := dockerCli.Client().NetworkRemove(ctx, network.ID); err != nil {
|
|
||||||
hasError = true
|
|
||||||
fmt.Fprintf(dockerCli.Err(), "Failed to remove network %s: %s", network.ID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hasError
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeSecrets(
|
|
||||||
ctx context.Context,
|
|
||||||
dockerCli command.Cli,
|
|
||||||
secrets []swarm.Secret,
|
|
||||||
) bool {
|
|
||||||
var hasError bool
|
|
||||||
for _, secret := range secrets {
|
|
||||||
fmt.Fprintf(dockerCli.Out(), "Removing secret %s\n", secret.Spec.Name)
|
|
||||||
if err := dockerCli.Client().SecretRemove(ctx, secret.ID); err != nil {
|
|
||||||
hasError = true
|
|
||||||
fmt.Fprintf(dockerCli.Err(), "Failed to remove secret %s: %s", secret.ID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hasError
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeConfigs(
|
|
||||||
ctx context.Context,
|
|
||||||
dockerCli command.Cli,
|
|
||||||
configs []swarm.Config,
|
|
||||||
) bool {
|
|
||||||
var hasError bool
|
|
||||||
for _, config := range configs {
|
|
||||||
fmt.Fprintf(dockerCli.Out(), "Removing config %s\n", config.Spec.Name)
|
|
||||||
if err := dockerCli.Client().ConfigRemove(ctx, config.ID); err != nil {
|
|
||||||
hasError = true
|
|
||||||
fmt.Fprintf(dockerCli.Err(), "Failed to remove config %s: %s", config.ID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hasError
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,94 +1,39 @@
|
||||||
package stack
|
package stack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/formatter"
|
"github.com/docker/cli/cli/command/stack/kubernetes"
|
||||||
"github.com/docker/cli/cli/command/service"
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/cli/command/stack/swarm"
|
||||||
"github.com/docker/docker/api/types"
|
cliopts "github.com/docker/cli/opts"
|
||||||
"github.com/docker/docker/api/types/filters"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/net/context"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type servicesOptions struct {
|
|
||||||
quiet bool
|
|
||||||
format string
|
|
||||||
filter opts.FilterOpt
|
|
||||||
namespace string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newServicesCommand(dockerCli command.Cli) *cobra.Command {
|
func newServicesCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
options := servicesOptions{filter: opts.NewFilterOpt()}
|
opts := options.Services{Filter: cliopts.NewFilterOpt()}
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "services [OPTIONS] STACK",
|
Use: "services [OPTIONS] STACK",
|
||||||
Short: "List the services in the stack",
|
Short: "List the services in the stack",
|
||||||
Args: cli.ExactArgs(1),
|
Args: cli.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
options.namespace = args[0]
|
opts.Namespace = args[0]
|
||||||
return runServices(dockerCli, options)
|
if dockerCli.ClientInfo().HasKubernetes() {
|
||||||
|
kli, err := kubernetes.WrapCli(dockerCli, cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return kubernetes.RunServices(kli, opts)
|
||||||
|
}
|
||||||
|
return swarm.RunServices(dockerCli, opts)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display IDs")
|
flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
|
||||||
flags.StringVar(&options.format, "format", "", "Pretty-print services using a Go template")
|
flags.StringVar(&opts.Format, "format", "", "Pretty-print services using a Go template")
|
||||||
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
|
flags.VarP(&opts.Filter, "filter", "f", "Filter output based on conditions provided")
|
||||||
|
flags.SetAnnotation("filter", "swarm", nil)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runServices(dockerCli command.Cli, options servicesOptions) error {
|
|
||||||
ctx := context.Background()
|
|
||||||
client := dockerCli.Client()
|
|
||||||
|
|
||||||
filter := getStackFilterFromOpt(options.namespace, options.filter)
|
|
||||||
services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: filter})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// if no services in this stack, print message and exit 0
|
|
||||||
if len(services) == 0 {
|
|
||||||
fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", options.namespace)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
info := map[string]formatter.ServiceListInfo{}
|
|
||||||
if !options.quiet {
|
|
||||||
taskFilter := filters.NewArgs()
|
|
||||||
for _, service := range services {
|
|
||||||
taskFilter.Add("service", service.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: taskFilter})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes, err := client.NodeList(ctx, types.NodeListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
info = service.GetServicesStatus(services, nodes, tasks)
|
|
||||||
}
|
|
||||||
|
|
||||||
format := options.format
|
|
||||||
if len(format) == 0 {
|
|
||||||
if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !options.quiet {
|
|
||||||
format = dockerCli.ConfigFile().ServicesFormat
|
|
||||||
} else {
|
|
||||||
format = formatter.TableFormatKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
servicesCtx := formatter.Context{
|
|
||||||
Output: dockerCli.Out(),
|
|
||||||
Format: formatter.NewServiceListFormat(format, options.quiet),
|
|
||||||
}
|
|
||||||
return formatter.ServiceListWrite(servicesCtx, services, info)
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,239 @@
|
||||||
|
package swarm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/compose/convert"
|
||||||
|
"github.com/docker/docker/api"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeClient struct {
|
||||||
|
client.Client
|
||||||
|
|
||||||
|
version string
|
||||||
|
|
||||||
|
services []string
|
||||||
|
networks []string
|
||||||
|
secrets []string
|
||||||
|
configs []string
|
||||||
|
|
||||||
|
removedServices []string
|
||||||
|
removedNetworks []string
|
||||||
|
removedSecrets []string
|
||||||
|
removedConfigs []string
|
||||||
|
|
||||||
|
serviceListFunc func(options types.ServiceListOptions) ([]swarm.Service, error)
|
||||||
|
networkListFunc func(options types.NetworkListOptions) ([]types.NetworkResource, error)
|
||||||
|
secretListFunc func(options types.SecretListOptions) ([]swarm.Secret, error)
|
||||||
|
configListFunc func(options types.ConfigListOptions) ([]swarm.Config, error)
|
||||||
|
nodeListFunc func(options types.NodeListOptions) ([]swarm.Node, error)
|
||||||
|
taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error)
|
||||||
|
nodeInspectWithRaw func(ref string) (swarm.Node, []byte, error)
|
||||||
|
|
||||||
|
serviceUpdateFunc func(serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error)
|
||||||
|
|
||||||
|
serviceRemoveFunc func(serviceID string) error
|
||||||
|
networkRemoveFunc func(networkID string) error
|
||||||
|
secretRemoveFunc func(secretID string) error
|
||||||
|
configRemoveFunc func(configID string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *fakeClient) ServerVersion(ctx context.Context) (types.Version, error) {
|
||||||
|
return types.Version{
|
||||||
|
Version: "docker-dev",
|
||||||
|
APIVersion: api.DefaultVersion,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *fakeClient) ClientVersion() string {
|
||||||
|
return cli.version
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *fakeClient) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||||
|
if cli.serviceListFunc != nil {
|
||||||
|
return cli.serviceListFunc(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace := namespaceFromFilters(options.Filters)
|
||||||
|
servicesList := []swarm.Service{}
|
||||||
|
for _, name := range cli.services {
|
||||||
|
if belongToNamespace(name, namespace) {
|
||||||
|
servicesList = append(servicesList, serviceFromName(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return servicesList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *fakeClient) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
|
||||||
|
if cli.networkListFunc != nil {
|
||||||
|
return cli.networkListFunc(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace := namespaceFromFilters(options.Filters)
|
||||||
|
networksList := []types.NetworkResource{}
|
||||||
|
for _, name := range cli.networks {
|
||||||
|
if belongToNamespace(name, namespace) {
|
||||||
|
networksList = append(networksList, networkFromName(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return networksList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *fakeClient) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) {
|
||||||
|
if cli.secretListFunc != nil {
|
||||||
|
return cli.secretListFunc(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace := namespaceFromFilters(options.Filters)
|
||||||
|
secretsList := []swarm.Secret{}
|
||||||
|
for _, name := range cli.secrets {
|
||||||
|
if belongToNamespace(name, namespace) {
|
||||||
|
secretsList = append(secretsList, secretFromName(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return secretsList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *fakeClient) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
|
if cli.configListFunc != nil {
|
||||||
|
return cli.configListFunc(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace := namespaceFromFilters(options.Filters)
|
||||||
|
configsList := []swarm.Config{}
|
||||||
|
for _, name := range cli.configs {
|
||||||
|
if belongToNamespace(name, namespace) {
|
||||||
|
configsList = append(configsList, configFromName(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return configsList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *fakeClient) TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error) {
|
||||||
|
if cli.taskListFunc != nil {
|
||||||
|
return cli.taskListFunc(options)
|
||||||
|
}
|
||||||
|
return []swarm.Task{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) {
|
||||||
|
if cli.nodeListFunc != nil {
|
||||||
|
return cli.nodeListFunc(options)
|
||||||
|
}
|
||||||
|
return []swarm.Node{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *fakeClient) NodeInspectWithRaw(ctx context.Context, ref string) (swarm.Node, []byte, error) {
|
||||||
|
if cli.nodeInspectWithRaw != nil {
|
||||||
|
return cli.nodeInspectWithRaw(ref)
|
||||||
|
}
|
||||||
|
return swarm.Node{}, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *fakeClient) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) {
|
||||||
|
if cli.serviceUpdateFunc != nil {
|
||||||
|
return cli.serviceUpdateFunc(serviceID, version, service, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.ServiceUpdateResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *fakeClient) ServiceRemove(ctx context.Context, serviceID string) error {
|
||||||
|
if cli.serviceRemoveFunc != nil {
|
||||||
|
return cli.serviceRemoveFunc(serviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli.removedServices = append(cli.removedServices, serviceID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *fakeClient) NetworkRemove(ctx context.Context, networkID string) error {
|
||||||
|
if cli.networkRemoveFunc != nil {
|
||||||
|
return cli.networkRemoveFunc(networkID)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli.removedNetworks = append(cli.removedNetworks, networkID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *fakeClient) SecretRemove(ctx context.Context, secretID string) error {
|
||||||
|
if cli.secretRemoveFunc != nil {
|
||||||
|
return cli.secretRemoveFunc(secretID)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli.removedSecrets = append(cli.removedSecrets, secretID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *fakeClient) ConfigRemove(ctx context.Context, configID string) error {
|
||||||
|
if cli.configRemoveFunc != nil {
|
||||||
|
return cli.configRemoveFunc(configID)
|
||||||
|
}
|
||||||
|
|
||||||
|
cli.removedConfigs = append(cli.removedConfigs, configID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func serviceFromName(name string) swarm.Service {
|
||||||
|
return swarm.Service{
|
||||||
|
ID: "ID-" + name,
|
||||||
|
Spec: swarm.ServiceSpec{
|
||||||
|
Annotations: swarm.Annotations{Name: name},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func networkFromName(name string) types.NetworkResource {
|
||||||
|
return types.NetworkResource{
|
||||||
|
ID: "ID-" + name,
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func secretFromName(name string) swarm.Secret {
|
||||||
|
return swarm.Secret{
|
||||||
|
ID: "ID-" + name,
|
||||||
|
Spec: swarm.SecretSpec{
|
||||||
|
Annotations: swarm.Annotations{Name: name},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func configFromName(name string) swarm.Config {
|
||||||
|
return swarm.Config{
|
||||||
|
ID: "ID-" + name,
|
||||||
|
Spec: swarm.ConfigSpec{
|
||||||
|
Annotations: swarm.Annotations{Name: name},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func namespaceFromFilters(filters filters.Args) string {
|
||||||
|
label := filters.Get("label")[0]
|
||||||
|
return strings.TrimPrefix(label, convert.LabelNamespace+"=")
|
||||||
|
}
|
||||||
|
|
||||||
|
func belongToNamespace(id, namespace string) bool {
|
||||||
|
return strings.HasPrefix(id, namespace+"_")
|
||||||
|
}
|
||||||
|
|
||||||
|
func objectName(namespace, name string) string {
|
||||||
|
return namespace + "_" + name
|
||||||
|
}
|
||||||
|
|
||||||
|
func objectID(name string) string {
|
||||||
|
return "ID-" + name
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildObjectIDs(objectNames []string) []string {
|
||||||
|
IDs := make([]string, len(objectNames))
|
||||||
|
for i, name := range objectNames {
|
||||||
|
IDs[i] = objectID(name)
|
||||||
|
}
|
||||||
|
return IDs
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package stack
|
package swarm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/docker/cli/cli/compose/convert"
|
"github.com/docker/cli/cli/compose/convert"
|
|
@ -0,0 +1,88 @@
|
||||||
|
package swarm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
|
"github.com/docker/cli/cli/compose/convert"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/api/types/versions"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resolve image constants
|
||||||
|
const (
|
||||||
|
defaultNetworkDriver = "overlay"
|
||||||
|
ResolveImageAlways = "always"
|
||||||
|
ResolveImageChanged = "changed"
|
||||||
|
ResolveImageNever = "never"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunDeploy is the swarm implementation of docker stack deploy
|
||||||
|
func RunDeploy(dockerCli command.Cli, opts options.Deploy) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if err := validateResolveImageFlag(dockerCli, &opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case opts.Bundlefile == "" && opts.Composefile == "":
|
||||||
|
return errors.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).")
|
||||||
|
case opts.Bundlefile != "" && opts.Composefile != "":
|
||||||
|
return errors.Errorf("You cannot specify both a bundle file and a Compose file.")
|
||||||
|
case opts.Bundlefile != "":
|
||||||
|
return deployBundle(ctx, dockerCli, opts)
|
||||||
|
default:
|
||||||
|
return deployCompose(ctx, dockerCli, opts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateResolveImageFlag validates the opts.resolveImage command line option
|
||||||
|
// and also turns image resolution off if the version is older than 1.30
|
||||||
|
func validateResolveImageFlag(dockerCli command.Cli, opts *options.Deploy) error {
|
||||||
|
if opts.ResolveImage != ResolveImageAlways && opts.ResolveImage != ResolveImageChanged && opts.ResolveImage != ResolveImageNever {
|
||||||
|
return errors.Errorf("Invalid option %s for flag --resolve-image", opts.ResolveImage)
|
||||||
|
}
|
||||||
|
// client side image resolution should not be done when the supported
|
||||||
|
// server version is older than 1.30
|
||||||
|
if versions.LessThan(dockerCli.Client().ClientVersion(), "1.30") {
|
||||||
|
opts.ResolveImage = ResolveImageNever
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkDaemonIsSwarmManager does an Info API call to verify that the daemon is
|
||||||
|
// a swarm manager. This is necessary because we must create networks before we
|
||||||
|
// create services, but the API call for creating a network does not return a
|
||||||
|
// proper status code when it can't create a network in the "global" scope.
|
||||||
|
func checkDaemonIsSwarmManager(ctx context.Context, dockerCli command.Cli) error {
|
||||||
|
info, err := dockerCli.Client().Info(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.Swarm.ControlAvailable {
|
||||||
|
return errors.New("this node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pruneServices removes services that are no longer referenced in the source
|
||||||
|
func pruneServices(ctx context.Context, dockerCli command.Cli, namespace convert.Namespace, services map[string]struct{}) {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
|
||||||
|
oldServices, err := getServices(ctx, client, namespace.Name())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "Failed to list services: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneServices := []swarm.Service{}
|
||||||
|
for _, service := range oldServices {
|
||||||
|
if _, exists := services[namespace.Descope(service.Spec.Name)]; !exists {
|
||||||
|
pruneServices = append(pruneServices, service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
removeServices(ctx, dockerCli, pruneServices)
|
||||||
|
}
|
|
@ -1,16 +1,23 @@
|
||||||
package stack
|
package swarm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/cli/cli/command/bundlefile"
|
||||||
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
"github.com/docker/cli/cli/compose/convert"
|
"github.com/docker/cli/cli/compose/convert"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func deployBundle(ctx context.Context, dockerCli command.Cli, opts deployOptions) error {
|
func deployBundle(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error {
|
||||||
bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile)
|
bundle, err := loadBundlefile(dockerCli.Err(), opts.Namespace, opts.Bundlefile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -19,9 +26,9 @@ func deployBundle(ctx context.Context, dockerCli command.Cli, opts deployOptions
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace := convert.NewNamespace(opts.namespace)
|
namespace := convert.NewNamespace(opts.Namespace)
|
||||||
|
|
||||||
if opts.prune {
|
if opts.Prune {
|
||||||
services := map[string]struct{}{}
|
services := map[string]struct{}{}
|
||||||
for service := range bundle.Services {
|
for service := range bundle.Services {
|
||||||
services[service] = struct{}{}
|
services[service] = struct{}{}
|
||||||
|
@ -87,5 +94,31 @@ func deployBundle(ctx context.Context, dockerCli command.Cli, opts deployOptions
|
||||||
if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil {
|
if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth, opts.resolveImage)
|
return deployServices(ctx, dockerCli, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefile.Bundlefile, error) {
|
||||||
|
defaultPath := fmt.Sprintf("%s.dab", namespace)
|
||||||
|
|
||||||
|
if path == "" {
|
||||||
|
path = defaultPath
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
return nil, errors.Errorf(
|
||||||
|
"Bundle %s not found. Specify the path with --file",
|
||||||
|
path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(stderr, "Loading bundle from %s\n", path)
|
||||||
|
reader, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
bundle, err := bundlefile.LoadFile(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Errorf("Error reading %s: %v\n", path, err)
|
||||||
|
}
|
||||||
|
return bundle, err
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package stack
|
package swarm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
|
@ -1,4 +1,4 @@
|
||||||
package stack
|
package swarm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -10,6 +10,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
"github.com/docker/cli/cli/compose/convert"
|
"github.com/docker/cli/cli/compose/convert"
|
||||||
"github.com/docker/cli/cli/compose/loader"
|
"github.com/docker/cli/cli/compose/loader"
|
||||||
composetypes "github.com/docker/cli/cli/compose/types"
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
|
@ -22,8 +23,8 @@ import (
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
func deployCompose(ctx context.Context, dockerCli command.Cli, opts deployOptions) error {
|
func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error {
|
||||||
configDetails, err := getConfigDetails(opts.composefile, dockerCli.In())
|
configDetails, err := getConfigDetails(opts.Composefile, dockerCli.In())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -54,9 +55,9 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts deployOption
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace := convert.NewNamespace(opts.namespace)
|
namespace := convert.NewNamespace(opts.Namespace)
|
||||||
|
|
||||||
if opts.prune {
|
if opts.Prune {
|
||||||
services := map[string]struct{}{}
|
services := map[string]struct{}{}
|
||||||
for _, service := range config.Services {
|
for _, service := range config.Services {
|
||||||
services[service.Name] = struct{}{}
|
services[service.Name] = struct{}{}
|
||||||
|
@ -93,7 +94,7 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts deployOption
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth, opts.resolveImage)
|
return deployServices(ctx, dockerCli, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
|
func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
|
||||||
|
@ -339,7 +340,7 @@ func deployServices(
|
||||||
updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth}
|
updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case resolveImage == resolveImageAlways || (resolveImage == resolveImageChanged && image != service.Spec.Labels[convert.LabelImage]):
|
case resolveImage == ResolveImageAlways || (resolveImage == ResolveImageChanged && image != service.Spec.Labels[convert.LabelImage]):
|
||||||
// image should be updated by the server using QueryRegistry
|
// image should be updated by the server using QueryRegistry
|
||||||
updateOpts.QueryRegistry = true
|
updateOpts.QueryRegistry = true
|
||||||
case image == service.Spec.Labels[convert.LabelImage]:
|
case image == service.Spec.Labels[convert.LabelImage]:
|
||||||
|
@ -369,7 +370,7 @@ func deployServices(
|
||||||
createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth}
|
createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth}
|
||||||
|
|
||||||
// query registry if flag disabling it was not set
|
// query registry if flag disabling it was not set
|
||||||
if resolveImage == resolveImageAlways || resolveImage == resolveImageChanged {
|
if resolveImage == ResolveImageAlways || resolveImage == ResolveImageChanged {
|
||||||
createOpts.QueryRegistry = true
|
createOpts.QueryRegistry = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package stack
|
package swarm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
|
@ -1,4 +1,4 @@
|
||||||
package stack
|
package swarm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -92,7 +92,7 @@ func TestServiceUpdateResolveImageChanged(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := deployServices(ctx, client, spec, namespace, false, resolveImageChanged)
|
err := deployServices(ctx, client, spec, namespace, false, ResolveImageChanged)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, testcase.expectedQueryRegistry, receivedOptions.QueryRegistry)
|
assert.Equal(t, testcase.expectedQueryRegistry, receivedOptions.QueryRegistry)
|
||||||
assert.Equal(t, testcase.expectedImage, receivedService.TaskTemplate.ContainerSpec.Image)
|
assert.Equal(t, testcase.expectedImage, receivedService.TaskTemplate.ContainerSpec.Image)
|
|
@ -0,0 +1,74 @@
|
||||||
|
package swarm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
|
"github.com/docker/cli/cli/compose/convert"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"vbom.ml/util/sortorder"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunList is the swarm implementation of docker stack ls
|
||||||
|
func RunList(dockerCli command.Cli, opts options.List) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
stacks, err := getStacks(ctx, client)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
format := opts.Format
|
||||||
|
if len(format) == 0 {
|
||||||
|
format = formatter.TableFormatKey
|
||||||
|
}
|
||||||
|
stackCtx := formatter.Context{
|
||||||
|
Output: dockerCli.Out(),
|
||||||
|
Format: formatter.NewStackFormat(format),
|
||||||
|
}
|
||||||
|
sort.Sort(byName(stacks))
|
||||||
|
return formatter.StackWrite(stackCtx, stacks)
|
||||||
|
}
|
||||||
|
|
||||||
|
type byName []*formatter.Stack
|
||||||
|
|
||||||
|
func (n byName) Len() int { return len(n) }
|
||||||
|
func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
|
||||||
|
func (n byName) Less(i, j int) bool { return sortorder.NaturalLess(n[i].Name, n[j].Name) }
|
||||||
|
|
||||||
|
func getStacks(ctx context.Context, apiclient client.APIClient) ([]*formatter.Stack, error) {
|
||||||
|
services, err := apiclient.ServiceList(
|
||||||
|
ctx,
|
||||||
|
types.ServiceListOptions{Filters: getAllStacksFilter()})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m := make(map[string]*formatter.Stack)
|
||||||
|
for _, service := range services {
|
||||||
|
labels := service.Spec.Labels
|
||||||
|
name, ok := labels[convert.LabelNamespace]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("cannot get label %s for service %s",
|
||||||
|
convert.LabelNamespace, service.ID)
|
||||||
|
}
|
||||||
|
ztack, ok := m[name]
|
||||||
|
if !ok {
|
||||||
|
m[name] = &formatter.Stack{
|
||||||
|
Name: name,
|
||||||
|
Services: 1,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ztack.Services++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var stacks []*formatter.Stack
|
||||||
|
for _, stack := range m {
|
||||||
|
stacks = append(stacks, stack)
|
||||||
|
}
|
||||||
|
return stacks, nil
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package swarm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/cli/cli/command/idresolver"
|
||||||
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
|
"github.com/docker/cli/cli/command/task"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunPS is the swarm implementation of docker stack ps
|
||||||
|
func RunPS(dockerCli command.Cli, opts options.PS) error {
|
||||||
|
namespace := opts.Namespace
|
||||||
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
filter := getStackFilterFromOpt(opts.Namespace, opts.Filter)
|
||||||
|
|
||||||
|
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tasks) == 0 {
|
||||||
|
return fmt.Errorf("nothing found in stack: %s", namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
format := opts.Format
|
||||||
|
if len(format) == 0 {
|
||||||
|
format = task.DefaultFormat(dockerCli.ConfigFile(), opts.Quiet)
|
||||||
|
}
|
||||||
|
|
||||||
|
return task.Print(ctx, dockerCli, tasks, idresolver.New(client, opts.NoResolve), !opts.NoTrunc, opts.Quiet, format)
|
||||||
|
}
|
|
@ -0,0 +1,141 @@
|
||||||
|
package swarm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/api/types/versions"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunRemove is the swarm implementation of docker stack remove
|
||||||
|
func RunRemove(dockerCli command.Cli, opts options.Remove) error {
|
||||||
|
namespaces := opts.Namespaces
|
||||||
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var errs []string
|
||||||
|
for _, namespace := range namespaces {
|
||||||
|
services, err := getServices(ctx, client, namespace)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
networks, err := getStackNetworks(ctx, client, namespace)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var secrets []swarm.Secret
|
||||||
|
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.25") {
|
||||||
|
secrets, err = getStackSecrets(ctx, client, namespace)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var configs []swarm.Config
|
||||||
|
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.30") {
|
||||||
|
configs, err = getStackConfigs(ctx, client, namespace)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(services)+len(networks)+len(secrets)+len(configs) == 0 {
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", namespace)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
hasError := removeServices(ctx, dockerCli, services)
|
||||||
|
hasError = removeSecrets(ctx, dockerCli, secrets) || hasError
|
||||||
|
hasError = removeConfigs(ctx, dockerCli, configs) || hasError
|
||||||
|
hasError = removeNetworks(ctx, dockerCli, networks) || hasError
|
||||||
|
|
||||||
|
if hasError {
|
||||||
|
errs = append(errs, fmt.Sprintf("Failed to remove some resources from stack: %s", namespace))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return errors.Errorf(strings.Join(errs, "\n"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortServiceByName(services []swarm.Service) func(i, j int) bool {
|
||||||
|
return func(i, j int) bool {
|
||||||
|
return services[i].Spec.Name < services[j].Spec.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeServices(
|
||||||
|
ctx context.Context,
|
||||||
|
dockerCli command.Cli,
|
||||||
|
services []swarm.Service,
|
||||||
|
) bool {
|
||||||
|
var hasError bool
|
||||||
|
sort.Slice(services, sortServiceByName(services))
|
||||||
|
for _, service := range services {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "Removing service %s\n", service.Spec.Name)
|
||||||
|
if err := dockerCli.Client().ServiceRemove(ctx, service.ID); err != nil {
|
||||||
|
hasError = true
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "Failed to remove service %s: %s", service.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasError
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeNetworks(
|
||||||
|
ctx context.Context,
|
||||||
|
dockerCli command.Cli,
|
||||||
|
networks []types.NetworkResource,
|
||||||
|
) bool {
|
||||||
|
var hasError bool
|
||||||
|
for _, network := range networks {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "Removing network %s\n", network.Name)
|
||||||
|
if err := dockerCli.Client().NetworkRemove(ctx, network.ID); err != nil {
|
||||||
|
hasError = true
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "Failed to remove network %s: %s", network.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasError
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeSecrets(
|
||||||
|
ctx context.Context,
|
||||||
|
dockerCli command.Cli,
|
||||||
|
secrets []swarm.Secret,
|
||||||
|
) bool {
|
||||||
|
var hasError bool
|
||||||
|
for _, secret := range secrets {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "Removing secret %s\n", secret.Spec.Name)
|
||||||
|
if err := dockerCli.Client().SecretRemove(ctx, secret.ID); err != nil {
|
||||||
|
hasError = true
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "Failed to remove secret %s: %s", secret.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasError
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeConfigs(
|
||||||
|
ctx context.Context,
|
||||||
|
dockerCli command.Cli,
|
||||||
|
configs []swarm.Config,
|
||||||
|
) bool {
|
||||||
|
var hasError bool
|
||||||
|
for _, config := range configs {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "Removing config %s\n", config.Spec.Name)
|
||||||
|
if err := dockerCli.Client().ConfigRemove(ctx, config.ID); err != nil {
|
||||||
|
hasError = true
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "Failed to remove config %s: %s", config.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasError
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package swarm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
|
"github.com/docker/cli/cli/command/service"
|
||||||
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunServices is the swarm implementation of docker stack services
|
||||||
|
func RunServices(dockerCli command.Cli, opts options.Services) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
client := dockerCli.Client()
|
||||||
|
|
||||||
|
filter := getStackFilterFromOpt(opts.Namespace, opts.Filter)
|
||||||
|
services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: filter})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no services in this stack, print message and exit 0
|
||||||
|
if len(services) == 0 {
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", opts.Namespace)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
info := map[string]formatter.ServiceListInfo{}
|
||||||
|
if !opts.Quiet {
|
||||||
|
taskFilter := filters.NewArgs()
|
||||||
|
for _, service := range services {
|
||||||
|
taskFilter.Add("service", service.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: taskFilter})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes, err := client.NodeList(ctx, types.NodeListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
info = service.GetServicesStatus(services, nodes, tasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
format := opts.Format
|
||||||
|
if len(format) == 0 {
|
||||||
|
if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.Quiet {
|
||||||
|
format = dockerCli.ConfigFile().ServicesFormat
|
||||||
|
} else {
|
||||||
|
format = formatter.TableFormatKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
servicesCtx := formatter.Context{
|
||||||
|
Output: dockerCli.Out(),
|
||||||
|
Format: formatter.NewServiceListFormat(format, opts.Quiet),
|
||||||
|
}
|
||||||
|
return formatter.ServiceListWrite(servicesCtx, services, info)
|
||||||
|
}
|
|
@ -10,11 +10,14 @@ import (
|
||||||
// NewSwarmCommand returns a cobra command for `swarm` subcommands
|
// NewSwarmCommand returns a cobra command for `swarm` subcommands
|
||||||
func NewSwarmCommand(dockerCli command.Cli) *cobra.Command {
|
func NewSwarmCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "swarm",
|
Use: "swarm",
|
||||||
Short: "Manage Swarm",
|
Short: "Manage Swarm",
|
||||||
Args: cli.NoArgs,
|
Args: cli.NoArgs,
|
||||||
RunE: command.ShowHelp(dockerCli.Err()),
|
RunE: command.ShowHelp(dockerCli.Err()),
|
||||||
Annotations: map[string]string{"version": "1.24"},
|
Annotations: map[string]string{
|
||||||
|
"version": "1.24",
|
||||||
|
"swarm": "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newInitCommand(dockerCli),
|
newInitCommand(dockerCli),
|
||||||
|
|
|
@ -1,13 +1,20 @@
|
||||||
package system
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
|
"golang.org/x/net/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeClient struct {
|
type fakeClient struct {
|
||||||
client.Client
|
client.Client
|
||||||
|
|
||||||
version string
|
version string
|
||||||
|
serverVersion func(ctx context.Context) (types.Version, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *fakeClient) ServerVersion(ctx context.Context) (types.Version, error) {
|
||||||
|
return cli.serverVersion(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cli *fakeClient) ClientVersion() string {
|
func (cli *fakeClient) ClientVersion() string {
|
||||||
|
|
|
@ -24,6 +24,7 @@ Client:{{if ne .Platform.Name ""}} {{.Platform.Name}}{{end}}
|
||||||
Built: {{.BuildTime}}
|
Built: {{.BuildTime}}
|
||||||
OS/Arch: {{.Os}}/{{.Arch}}
|
OS/Arch: {{.Os}}/{{.Arch}}
|
||||||
Experimental: {{.Experimental}}
|
Experimental: {{.Experimental}}
|
||||||
|
Orchestrator: {{.Orchestrator}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
|
|
||||||
{{- if .ServerOK}}{{with .Server}}
|
{{- if .ServerOK}}{{with .Server}}
|
||||||
|
@ -71,6 +72,7 @@ type clientVersion struct {
|
||||||
Arch string
|
Arch string
|
||||||
BuildTime string `json:",omitempty"`
|
BuildTime string `json:",omitempty"`
|
||||||
Experimental bool
|
Experimental bool
|
||||||
|
Orchestrator string `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerOK returns true when the client could connect to the docker server
|
// ServerOK returns true when the client could connect to the docker server
|
||||||
|
@ -80,7 +82,7 @@ func (v versionInfo) ServerOK() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewVersionCommand creates a new cobra.Command for `docker version`
|
// NewVersionCommand creates a new cobra.Command for `docker version`
|
||||||
func NewVersionCommand(dockerCli *command.DockerCli) *cobra.Command {
|
func NewVersionCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
var opts versionOptions
|
var opts versionOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
|
@ -107,9 +109,7 @@ func reformatDate(buildTime string) string {
|
||||||
return buildTime
|
return buildTime
|
||||||
}
|
}
|
||||||
|
|
||||||
func runVersion(dockerCli *command.DockerCli, opts *versionOptions) error {
|
func runVersion(dockerCli command.Cli, opts *versionOptions) error {
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
templateFormat := versionTemplate
|
templateFormat := versionTemplate
|
||||||
tmpl := templates.New("version")
|
tmpl := templates.New("version")
|
||||||
if opts.format != "" {
|
if opts.format != "" {
|
||||||
|
@ -127,23 +127,21 @@ func runVersion(dockerCli *command.DockerCli, opts *versionOptions) error {
|
||||||
|
|
||||||
vd := versionInfo{
|
vd := versionInfo{
|
||||||
Client: clientVersion{
|
Client: clientVersion{
|
||||||
|
Platform: struct{ Name string }{cli.PlatformName},
|
||||||
Version: cli.Version,
|
Version: cli.Version,
|
||||||
APIVersion: dockerCli.Client().ClientVersion(),
|
APIVersion: dockerCli.Client().ClientVersion(),
|
||||||
DefaultAPIVersion: dockerCli.DefaultVersion(),
|
DefaultAPIVersion: dockerCli.DefaultVersion(),
|
||||||
GoVersion: runtime.Version(),
|
GoVersion: runtime.Version(),
|
||||||
GitCommit: cli.GitCommit,
|
GitCommit: cli.GitCommit,
|
||||||
BuildTime: cli.BuildTime,
|
BuildTime: reformatDate(cli.BuildTime),
|
||||||
Os: runtime.GOOS,
|
Os: runtime.GOOS,
|
||||||
Arch: runtime.GOARCH,
|
Arch: runtime.GOARCH,
|
||||||
Experimental: dockerCli.ClientInfo().HasExperimental,
|
Experimental: dockerCli.ClientInfo().HasExperimental,
|
||||||
|
Orchestrator: string(dockerCli.ClientInfo().Orchestrator),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
vd.Client.Platform.Name = cli.PlatformName
|
|
||||||
|
|
||||||
// first we need to make BuildTime more human friendly
|
sv, err := dockerCli.Client().ServerVersion(context.Background())
|
||||||
vd.Client.BuildTime = reformatDate(vd.Client.BuildTime)
|
|
||||||
|
|
||||||
sv, err := dockerCli.Client().ServerVersion(ctx)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
vd.Server = &sv
|
vd.Server = &sv
|
||||||
foundEngine := false
|
foundEngine := false
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
|
|
||||||
|
"github.com/docker/cli/internal/test"
|
||||||
|
"github.com/docker/docker/api"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVersionWithoutServer(t *testing.T) {
|
||||||
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
|
serverVersion: func(ctx context.Context) (types.Version, error) {
|
||||||
|
return types.Version{}, fmt.Errorf("no server")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
cmd := NewVersionCommand(cli)
|
||||||
|
cmd.SetOutput(cli.Err())
|
||||||
|
assert.Error(t, cmd.Execute())
|
||||||
|
assert.Contains(t, cleanTabs(cli.OutBuffer().String()), "Client:")
|
||||||
|
assert.NotContains(t, cleanTabs(cli.OutBuffer().String()), "Server:")
|
||||||
|
}
|
||||||
|
|
||||||
|
func fakeServerVersion(ctx context.Context) (types.Version, error) {
|
||||||
|
return types.Version{
|
||||||
|
Version: "docker-dev",
|
||||||
|
APIVersion: api.DefaultVersion,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVersionWithOrchestrator(t *testing.T) {
|
||||||
|
cli := test.NewFakeCli(&fakeClient{serverVersion: fakeServerVersion})
|
||||||
|
cli.SetClientInfo(func() command.ClientInfo { return command.ClientInfo{Orchestrator: "swarm"} })
|
||||||
|
cmd := NewVersionCommand(cli)
|
||||||
|
assert.NoError(t, cmd.Execute())
|
||||||
|
assert.Contains(t, cleanTabs(cli.OutBuffer().String()), "Orchestrator: swarm")
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanTabs(line string) string {
|
||||||
|
return strings.Join(strings.Fields(line), " ")
|
||||||
|
}
|
|
@ -45,6 +45,7 @@ type ConfigFile struct {
|
||||||
PruneFilters []string `json:"pruneFilters,omitempty"`
|
PruneFilters []string `json:"pruneFilters,omitempty"`
|
||||||
Proxies map[string]ProxyConfig `json:"proxies,omitempty"`
|
Proxies map[string]ProxyConfig `json:"proxies,omitempty"`
|
||||||
Experimental string `json:"experimental,omitempty"`
|
Experimental string `json:"experimental,omitempty"`
|
||||||
|
Orchestrator string `json:"orchestrator,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProxyConfig contains proxy configuration settings
|
// ProxyConfig contains proxy configuration settings
|
||||||
|
|
|
@ -30,12 +30,13 @@ var (
|
||||||
|
|
||||||
// CommonOptions are options common to both the client and the daemon.
|
// CommonOptions are options common to both the client and the daemon.
|
||||||
type CommonOptions struct {
|
type CommonOptions struct {
|
||||||
Debug bool
|
Debug bool
|
||||||
Hosts []string
|
Hosts []string
|
||||||
LogLevel string
|
Orchestrator string
|
||||||
TLS bool
|
LogLevel string
|
||||||
TLSVerify bool
|
TLS bool
|
||||||
TLSOptions *tlsconfig.Options
|
TLSVerify bool
|
||||||
|
TLSOptions *tlsconfig.Options
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCommonOptions returns a new CommonOptions
|
// NewCommonOptions returns a new CommonOptions
|
||||||
|
@ -53,6 +54,8 @@ func (commonOpts *CommonOptions) InstallFlags(flags *pflag.FlagSet) {
|
||||||
flags.StringVarP(&commonOpts.LogLevel, "log-level", "l", "info", `Set the logging level ("debug"|"info"|"warn"|"error"|"fatal")`)
|
flags.StringVarP(&commonOpts.LogLevel, "log-level", "l", "info", `Set the logging level ("debug"|"info"|"warn"|"error"|"fatal")`)
|
||||||
flags.BoolVar(&commonOpts.TLS, "tls", false, "Use TLS; implied by --tlsverify")
|
flags.BoolVar(&commonOpts.TLS, "tls", false, "Use TLS; implied by --tlsverify")
|
||||||
flags.BoolVar(&commonOpts.TLSVerify, FlagTLSVerify, dockerTLSVerify, "Use TLS and verify the remote")
|
flags.BoolVar(&commonOpts.TLSVerify, FlagTLSVerify, dockerTLSVerify, "Use TLS and verify the remote")
|
||||||
|
flags.StringVar(&commonOpts.Orchestrator, "orchestrator", "", "Which orchestrator to use with the docker cli (swarm|kubernetes) (default swarm) (experimental)")
|
||||||
|
flags.SetAnnotation("orchestrator", "experimentalCLI", nil)
|
||||||
|
|
||||||
// TODO use flag flags.String("identity"}, "i", "", "Path to libtrust key file")
|
// TODO use flag flags.String("identity"}, "i", "", "Path to libtrust key file")
|
||||||
|
|
||||||
|
|
|
@ -197,25 +197,36 @@ type versionDetails interface {
|
||||||
ServerInfo() command.ServerInfo
|
ServerInfo() command.ServerInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hideFeatureFlag(f *pflag.Flag, hasFeature bool, annotation string) {
|
||||||
|
if hasFeature {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := f.Annotations[annotation]; ok {
|
||||||
|
f.Hidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hideFeatureSubCommand(subcmd *cobra.Command, hasFeature bool, annotation string) {
|
||||||
|
if hasFeature {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := subcmd.Annotations[annotation]; ok {
|
||||||
|
subcmd.Hidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) {
|
func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) {
|
||||||
clientVersion := details.Client().ClientVersion()
|
clientVersion := details.Client().ClientVersion()
|
||||||
osType := details.ServerInfo().OSType
|
osType := details.ServerInfo().OSType
|
||||||
hasExperimental := details.ServerInfo().HasExperimental
|
hasExperimental := details.ServerInfo().HasExperimental
|
||||||
hasExperimentalCLI := details.ClientInfo().HasExperimental
|
hasExperimentalCLI := details.ClientInfo().HasExperimental
|
||||||
|
hasKubernetes := details.ClientInfo().HasKubernetes()
|
||||||
|
|
||||||
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||||
// hide experimental flags
|
hideFeatureFlag(f, hasExperimental, "experimental")
|
||||||
if !hasExperimental {
|
hideFeatureFlag(f, hasExperimentalCLI, "experimentalCLI")
|
||||||
if _, ok := f.Annotations["experimental"]; ok {
|
hideFeatureFlag(f, hasKubernetes, "kubernetes")
|
||||||
f.Hidden = true
|
hideFeatureFlag(f, !hasKubernetes, "swarm")
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasExperimentalCLI {
|
|
||||||
if _, ok := f.Annotations["experimentalCLI"]; ok {
|
|
||||||
f.Hidden = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// hide flags not supported by the server
|
// hide flags not supported by the server
|
||||||
if !isOSTypeSupported(f, osType) || !isVersionSupported(f, clientVersion) {
|
if !isOSTypeSupported(f, osType) || !isVersionSupported(f, clientVersion) {
|
||||||
f.Hidden = true
|
f.Hidden = true
|
||||||
|
@ -223,18 +234,10 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) {
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, subcmd := range cmd.Commands() {
|
for _, subcmd := range cmd.Commands() {
|
||||||
// hide experimental subcommands
|
hideFeatureSubCommand(subcmd, hasExperimental, "experimental")
|
||||||
if !hasExperimental {
|
hideFeatureSubCommand(subcmd, hasExperimentalCLI, "experimentalCLI")
|
||||||
if _, ok := subcmd.Annotations["experimental"]; ok {
|
hideFeatureSubCommand(subcmd, hasKubernetes, "kubernetes")
|
||||||
subcmd.Hidden = true
|
hideFeatureSubCommand(subcmd, !hasKubernetes, "swarm")
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasExperimentalCLI {
|
|
||||||
if _, ok := subcmd.Annotations["experimentalCLI"]; ok {
|
|
||||||
subcmd.Hidden = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// hide subcommands not supported by the server
|
// hide subcommands not supported by the server
|
||||||
if subcmdVersion, ok := subcmd.Annotations["version"]; ok && versions.LessThan(clientVersion, subcmdVersion) {
|
if subcmdVersion, ok := subcmd.Annotations["version"]; ok && versions.LessThan(clientVersion, subcmdVersion) {
|
||||||
subcmd.Hidden = true
|
subcmd.Hidden = true
|
||||||
|
@ -243,24 +246,24 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func isSupported(cmd *cobra.Command, details versionDetails) error {
|
func isSupported(cmd *cobra.Command, details versionDetails) error {
|
||||||
|
if err := areSubcommandsSupported(cmd, details); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := areFlagsSupported(cmd, details); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func areFlagsSupported(cmd *cobra.Command, details versionDetails) error {
|
||||||
clientVersion := details.Client().ClientVersion()
|
clientVersion := details.Client().ClientVersion()
|
||||||
osType := details.ServerInfo().OSType
|
osType := details.ServerInfo().OSType
|
||||||
hasExperimental := details.ServerInfo().HasExperimental
|
hasExperimental := details.ServerInfo().HasExperimental
|
||||||
|
hasKubernetes := details.ClientInfo().HasKubernetes()
|
||||||
hasExperimentalCLI := details.ClientInfo().HasExperimental
|
hasExperimentalCLI := details.ClientInfo().HasExperimental
|
||||||
|
|
||||||
// Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack`
|
|
||||||
for curr := cmd; curr != nil; curr = curr.Parent() {
|
|
||||||
if cmdVersion, ok := curr.Annotations["version"]; ok && versions.LessThan(clientVersion, cmdVersion) {
|
|
||||||
return fmt.Errorf("%s requires API version %s, but the Docker daemon API version is %s", cmd.CommandPath(), cmdVersion, clientVersion)
|
|
||||||
}
|
|
||||||
if _, ok := curr.Annotations["experimental"]; ok && !hasExperimental {
|
|
||||||
return fmt.Errorf("%s is only supported on a Docker daemon with experimental features enabled", cmd.CommandPath())
|
|
||||||
}
|
|
||||||
if _, ok := curr.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI {
|
|
||||||
return fmt.Errorf("%s is only supported when experimental cli features are enabled", cmd.CommandPath())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
errs := []string{}
|
errs := []string{}
|
||||||
|
|
||||||
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||||
|
@ -279,12 +282,45 @@ func isSupported(cmd *cobra.Command, details versionDetails) error {
|
||||||
if _, ok := f.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI {
|
if _, ok := f.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI {
|
||||||
errs = append(errs, fmt.Sprintf("\"--%s\" is only supported when experimental cli features are enabled", f.Name))
|
errs = append(errs, fmt.Sprintf("\"--%s\" is only supported when experimental cli features are enabled", f.Name))
|
||||||
}
|
}
|
||||||
|
if _, ok := f.Annotations["kubernetes"]; ok && !hasKubernetes {
|
||||||
|
errs = append(errs, fmt.Sprintf("\"--%s\" is only supported on a Docker cli with kubernetes features enabled", f.Name))
|
||||||
|
}
|
||||||
|
if _, ok := f.Annotations["swarm"]; ok && hasKubernetes {
|
||||||
|
errs = append(errs, fmt.Sprintf("\"--%s\" is only supported on a Docker cli with swarm features enabled", f.Name))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return errors.New(strings.Join(errs, "\n"))
|
return errors.New(strings.Join(errs, "\n"))
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack`
|
||||||
|
func areSubcommandsSupported(cmd *cobra.Command, details versionDetails) error {
|
||||||
|
clientVersion := details.Client().ClientVersion()
|
||||||
|
hasExperimental := details.ServerInfo().HasExperimental
|
||||||
|
hasExperimentalCLI := details.ClientInfo().HasExperimental
|
||||||
|
hasKubernetes := details.ClientInfo().HasKubernetes()
|
||||||
|
|
||||||
|
// Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack`
|
||||||
|
for curr := cmd; curr != nil; curr = curr.Parent() {
|
||||||
|
if cmdVersion, ok := curr.Annotations["version"]; ok && versions.LessThan(clientVersion, cmdVersion) {
|
||||||
|
return fmt.Errorf("%s requires API version %s, but the Docker daemon API version is %s", cmd.CommandPath(), cmdVersion, clientVersion)
|
||||||
|
}
|
||||||
|
if _, ok := curr.Annotations["experimental"]; ok && !hasExperimental {
|
||||||
|
return fmt.Errorf("%s is only supported on a Docker daemon with experimental features enabled", cmd.CommandPath())
|
||||||
|
}
|
||||||
|
if _, ok := curr.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI {
|
||||||
|
return fmt.Errorf("%s is only supported when experimental cli features are enabled", cmd.CommandPath())
|
||||||
|
}
|
||||||
|
if _, ok := curr.Annotations["kubernetes"]; ok && !hasKubernetes {
|
||||||
|
return fmt.Errorf("%s is only supported on a Docker cli with kubernetes features enabled", cmd.CommandPath())
|
||||||
|
}
|
||||||
|
if _, ok := curr.Annotations["swarm"]; ok && hasKubernetes {
|
||||||
|
return fmt.Errorf("%s is only supported on a Docker cli with swarm features enabled", cmd.CommandPath())
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,3 +17,6 @@ ignore:
|
||||||
- "**/internal/test"
|
- "**/internal/test"
|
||||||
- "vendor/*"
|
- "vendor/*"
|
||||||
- "cli/compose/schema/bindata.go"
|
- "cli/compose/schema/bindata.go"
|
||||||
|
- "cli/command/stack/kubernetes/api/openapi"
|
||||||
|
- "cli/command/stack/kubernetes/api/client"
|
||||||
|
- ".*generated.*"
|
|
@ -15,14 +15,17 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type cmdOption struct {
|
type cmdOption struct {
|
||||||
Option string
|
Option string
|
||||||
Shorthand string `yaml:",omitempty"`
|
Shorthand string `yaml:",omitempty"`
|
||||||
ValueType string `yaml:"value_type,omitempty"`
|
ValueType string `yaml:"value_type,omitempty"`
|
||||||
DefaultValue string `yaml:"default_value,omitempty"`
|
DefaultValue string `yaml:"default_value,omitempty"`
|
||||||
Description string `yaml:",omitempty"`
|
Description string `yaml:",omitempty"`
|
||||||
Deprecated bool
|
Deprecated bool
|
||||||
MinAPIVersion string `yaml:"min_api_version,omitempty"`
|
MinAPIVersion string `yaml:"min_api_version,omitempty"`
|
||||||
Experimental bool
|
Experimental bool
|
||||||
|
ExperimentalCLI bool
|
||||||
|
Kubernetes bool
|
||||||
|
Swarm bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type cmdDoc struct {
|
type cmdDoc struct {
|
||||||
|
@ -43,6 +46,9 @@ type cmdDoc struct {
|
||||||
Deprecated bool
|
Deprecated bool
|
||||||
MinAPIVersion string `yaml:"min_api_version,omitempty"`
|
MinAPIVersion string `yaml:"min_api_version,omitempty"`
|
||||||
Experimental bool
|
Experimental bool
|
||||||
|
ExperimentalCLI bool
|
||||||
|
Kubernetes bool
|
||||||
|
Swarm bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenYamlTree creates yaml structured ref files
|
// GenYamlTree creates yaml structured ref files
|
||||||
|
@ -110,6 +116,15 @@ func GenYamlCustom(cmd *cobra.Command, w io.Writer) error {
|
||||||
if _, ok := curr.Annotations["experimental"]; ok && !cliDoc.Experimental {
|
if _, ok := curr.Annotations["experimental"]; ok && !cliDoc.Experimental {
|
||||||
cliDoc.Experimental = true
|
cliDoc.Experimental = true
|
||||||
}
|
}
|
||||||
|
if _, ok := curr.Annotations["experimentalCLI"]; ok && !cliDoc.ExperimentalCLI {
|
||||||
|
cliDoc.ExperimentalCLI = true
|
||||||
|
}
|
||||||
|
if _, ok := curr.Annotations["kubernetes"]; ok && !cliDoc.Kubernetes {
|
||||||
|
cliDoc.Kubernetes = true
|
||||||
|
}
|
||||||
|
if _, ok := curr.Annotations["swarm"]; ok && !cliDoc.Swarm {
|
||||||
|
cliDoc.Kubernetes = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flags := cmd.NonInheritedFlags()
|
flags := cmd.NonInheritedFlags()
|
||||||
|
@ -186,6 +201,15 @@ func genFlagResult(flags *pflag.FlagSet) []cmdOption {
|
||||||
if v, ok := flag.Annotations["version"]; ok {
|
if v, ok := flag.Annotations["version"]; ok {
|
||||||
opt.MinAPIVersion = v[0]
|
opt.MinAPIVersion = v[0]
|
||||||
}
|
}
|
||||||
|
if _, ok := flag.Annotations["experimentalCLI"]; ok {
|
||||||
|
opt.ExperimentalCLI = true
|
||||||
|
}
|
||||||
|
if _, ok := flag.Annotations["kubernetes"]; ok {
|
||||||
|
opt.Kubernetes = true
|
||||||
|
}
|
||||||
|
if _, ok := flag.Annotations["swarm"]; ok {
|
||||||
|
opt.Kubernetes = true
|
||||||
|
}
|
||||||
|
|
||||||
result = append(result, opt)
|
result = append(result, opt)
|
||||||
})
|
})
|
||||||
|
|
|
@ -36,11 +36,21 @@ func deployFullStack(t *testing.T, stackname string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanupFullStack(t *testing.T, stackname string) {
|
func cleanupFullStack(t *testing.T, stackname string) {
|
||||||
result := icmd.RunCmd(shell(t, "docker stack rm %s", stackname))
|
// FIXME(vdemeester) we shouldn't have to do that. it is hidding a race on docker stack rm
|
||||||
result.Assert(t, icmd.Success)
|
poll.WaitOn(t, stackRm(stackname), pollSettings)
|
||||||
poll.WaitOn(t, taskCount(stackname, 0), pollSettings)
|
poll.WaitOn(t, taskCount(stackname, 0), pollSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stackRm(stackname string) func(t poll.LogT) poll.Result {
|
||||||
|
return func(poll.LogT) poll.Result {
|
||||||
|
result := icmd.RunCommand("docker", "stack", "rm", stackname)
|
||||||
|
if result.Error != nil {
|
||||||
|
return poll.Continue("docker stack rm %s failed : %v", stackname, result.Error)
|
||||||
|
}
|
||||||
|
return poll.Success()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func taskCount(stackname string, expected int) func(t poll.LogT) poll.Result {
|
func taskCount(stackname string, expected int) func(t poll.LogT) poll.Result {
|
||||||
return func(poll.LogT) poll.Result {
|
return func(poll.LogT) poll.Result {
|
||||||
result := icmd.RunCommand(
|
result := icmd.RunCommand(
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
"Sort": ["linter", "severity", "path", "line"],
|
"Sort": ["linter", "severity", "path", "line"],
|
||||||
"Exclude": [
|
"Exclude": [
|
||||||
"cli/compose/schema/bindata.go",
|
"cli/compose/schema/bindata.go",
|
||||||
|
"cli/command/stack/kubernetes/api/openapi",
|
||||||
|
"cli/command/stack/kubernetes/api/client",
|
||||||
|
".*generated.*",
|
||||||
"parameter .* always receives"
|
"parameter .* always receives"
|
||||||
],
|
],
|
||||||
"EnableGC": true,
|
"EnableGC": true,
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type notaryClientFuncType func(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
type notaryClientFuncType func(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
||||||
|
type clientInfoFuncType func() command.ClientInfo
|
||||||
|
|
||||||
// FakeCli emulates the default DockerCli
|
// FakeCli emulates the default DockerCli
|
||||||
type FakeCli struct {
|
type FakeCli struct {
|
||||||
|
@ -26,6 +27,7 @@ type FakeCli struct {
|
||||||
err *bytes.Buffer
|
err *bytes.Buffer
|
||||||
in *command.InStream
|
in *command.InStream
|
||||||
server command.ServerInfo
|
server command.ServerInfo
|
||||||
|
clientInfoFunc clientInfoFuncType
|
||||||
notaryClientFunc notaryClientFuncType
|
notaryClientFunc notaryClientFuncType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,6 +90,19 @@ func (c *FakeCli) ServerInfo() command.ServerInfo {
|
||||||
return c.server
|
return c.server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClientInfo returns client information
|
||||||
|
func (c *FakeCli) ClientInfo() command.ClientInfo {
|
||||||
|
if c.clientInfoFunc != nil {
|
||||||
|
return c.clientInfoFunc()
|
||||||
|
}
|
||||||
|
return c.DockerCli.ClientInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetClientInfo sets the internal getter for retrieving a ClientInfo
|
||||||
|
func (c *FakeCli) SetClientInfo(clientInfoFunc clientInfoFuncType) {
|
||||||
|
c.clientInfoFunc = clientInfoFunc
|
||||||
|
}
|
||||||
|
|
||||||
// OutBuffer returns the stdout buffer
|
// OutBuffer returns the stdout buffer
|
||||||
func (c *FakeCli) OutBuffer() *bytes.Buffer {
|
func (c *FakeCli) OutBuffer() *bytes.Buffer {
|
||||||
return c.outBuffer
|
return c.outBuffer
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Kubernetes client libraries
|
||||||
|
|
||||||
|
This package (and sub-packages) holds the client libraries for the kubernetes integration in
|
||||||
|
the docker platform. Most of the code is currently generated.
|
|
@ -0,0 +1,88 @@
|
||||||
|
package clientset
|
||||||
|
|
||||||
|
import (
|
||||||
|
composev1beta1 "github.com/docker/cli/kubernetes/client/clientset_generated/clientset/typed/compose/v1beta1"
|
||||||
|
glog "github.com/golang/glog"
|
||||||
|
discovery "k8s.io/client-go/discovery"
|
||||||
|
rest "k8s.io/client-go/rest"
|
||||||
|
flowcontrol "k8s.io/client-go/util/flowcontrol"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Interface interface {
|
||||||
|
Discovery() discovery.DiscoveryInterface
|
||||||
|
ComposeV1beta1() composev1beta1.ComposeV1beta1Interface
|
||||||
|
// Deprecated: please explicitly pick a version if possible.
|
||||||
|
Compose() composev1beta1.ComposeV1beta1Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clientset contains the clients for groups. Each group has exactly one
|
||||||
|
// version included in a Clientset.
|
||||||
|
type Clientset struct {
|
||||||
|
*discovery.DiscoveryClient
|
||||||
|
*composev1beta1.ComposeV1beta1Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComposeV1beta1 retrieves the ComposeV1beta1Client
|
||||||
|
func (c *Clientset) ComposeV1beta1() composev1beta1.ComposeV1beta1Interface {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.ComposeV1beta1Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Compose retrieves the default version of ComposeClient.
|
||||||
|
// Please explicitly pick a version.
|
||||||
|
func (c *Clientset) Compose() composev1beta1.ComposeV1beta1Interface {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.ComposeV1beta1Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discovery retrieves the DiscoveryClient
|
||||||
|
func (c *Clientset) Discovery() discovery.DiscoveryInterface {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.DiscoveryClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewForConfig creates a new Clientset for the given config.
|
||||||
|
func NewForConfig(c *rest.Config) (*Clientset, error) {
|
||||||
|
configShallowCopy := *c
|
||||||
|
if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 {
|
||||||
|
configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst)
|
||||||
|
}
|
||||||
|
var cs Clientset
|
||||||
|
var err error
|
||||||
|
cs.ComposeV1beta1Client, err = composev1beta1.NewForConfig(&configShallowCopy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy)
|
||||||
|
if err != nil {
|
||||||
|
glog.Errorf("failed to create the DiscoveryClient: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &cs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewForConfigOrDie creates a new Clientset for the given config and
|
||||||
|
// panics if there is an error in the config.
|
||||||
|
func NewForConfigOrDie(c *rest.Config) *Clientset {
|
||||||
|
var cs Clientset
|
||||||
|
cs.ComposeV1beta1Client = composev1beta1.NewForConfigOrDie(c)
|
||||||
|
|
||||||
|
cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c)
|
||||||
|
return &cs
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Clientset for the given RESTClient.
|
||||||
|
func New(c rest.Interface) *Clientset {
|
||||||
|
var cs Clientset
|
||||||
|
cs.ComposeV1beta1Client = composev1beta1.New(c)
|
||||||
|
|
||||||
|
cs.DiscoveryClient = discovery.NewDiscoveryClient(c)
|
||||||
|
return &cs
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
// This package is generated by client-gen with custom arguments.
|
||||||
|
|
||||||
|
// This package has the automatically generated clientset.
|
||||||
|
package clientset
|
|
@ -0,0 +1,4 @@
|
||||||
|
// This package is generated by client-gen with custom arguments.
|
||||||
|
|
||||||
|
// This package contains the scheme of the automatically generated clientset.
|
||||||
|
package scheme
|
|
@ -0,0 +1,37 @@
|
||||||
|
package scheme
|
||||||
|
|
||||||
|
import (
|
||||||
|
composev1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
|
||||||
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||||
|
schema "k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
serializer "k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Scheme = runtime.NewScheme()
|
||||||
|
var Codecs = serializer.NewCodecFactory(Scheme)
|
||||||
|
var ParameterCodec = runtime.NewParameterCodec(Scheme)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"})
|
||||||
|
AddToScheme(Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddToScheme adds all types of this clientset into the given scheme. This allows composition
|
||||||
|
// of clientsets, like in:
|
||||||
|
//
|
||||||
|
// import (
|
||||||
|
// "k8s.io/client-go/kubernetes"
|
||||||
|
// clientsetscheme "k8s.io/client-go/kuberentes/scheme"
|
||||||
|
// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// kclientset, _ := kubernetes.NewForConfig(c)
|
||||||
|
// aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
|
||||||
|
//
|
||||||
|
// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
|
||||||
|
// correctly.
|
||||||
|
func AddToScheme(scheme *runtime.Scheme) {
|
||||||
|
composev1beta1.AddToScheme(scheme)
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package v1beta1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/cli/kubernetes/client/clientset_generated/clientset/scheme"
|
||||||
|
v1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
|
||||||
|
serializer "k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
|
rest "k8s.io/client-go/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ComposeV1beta1Interface interface {
|
||||||
|
RESTClient() rest.Interface
|
||||||
|
StacksGetter
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComposeV1beta1Client is used to interact with features provided by the compose.docker.com group.
|
||||||
|
type ComposeV1beta1Client struct {
|
||||||
|
restClient rest.Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ComposeV1beta1Client) Stacks(namespace string) StackInterface {
|
||||||
|
return newStacks(c, namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewForConfig creates a new ComposeV1beta1Client for the given config.
|
||||||
|
func NewForConfig(c *rest.Config) (*ComposeV1beta1Client, error) {
|
||||||
|
config := *c
|
||||||
|
if err := setConfigDefaults(&config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
client, err := rest.RESTClientFor(&config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &ComposeV1beta1Client{client}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewForConfigOrDie creates a new ComposeV1beta1Client for the given config and
|
||||||
|
// panics if there is an error in the config.
|
||||||
|
func NewForConfigOrDie(c *rest.Config) *ComposeV1beta1Client {
|
||||||
|
client, err := NewForConfig(c)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new ComposeV1beta1Client for the given RESTClient.
|
||||||
|
func New(c rest.Interface) *ComposeV1beta1Client {
|
||||||
|
return &ComposeV1beta1Client{c}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setConfigDefaults(config *rest.Config) error {
|
||||||
|
gv := v1beta1.SchemeGroupVersion
|
||||||
|
config.GroupVersion = &gv
|
||||||
|
config.APIPath = "/apis"
|
||||||
|
config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: scheme.Codecs}
|
||||||
|
|
||||||
|
if config.UserAgent == "" {
|
||||||
|
config.UserAgent = rest.DefaultKubernetesUserAgent()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RESTClient returns a RESTClient that is used to communicate
|
||||||
|
// with API server by this client implementation.
|
||||||
|
func (c *ComposeV1beta1Client) RESTClient() rest.Interface {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.restClient
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
// This package is generated by client-gen with custom arguments.
|
||||||
|
|
||||||
|
// This package has the automatically generated typed clients.
|
||||||
|
package v1beta1
|
|
@ -0,0 +1,3 @@
|
||||||
|
package v1beta1
|
||||||
|
|
||||||
|
type StackExpansion interface{}
|
|
@ -0,0 +1,158 @@
|
||||||
|
package v1beta1
|
||||||
|
|
||||||
|
import (
|
||||||
|
scheme "github.com/docker/cli/kubernetes/client/clientset_generated/clientset/scheme"
|
||||||
|
v1beta1 "github.com/docker/cli/kubernetes/compose/v1beta1"
|
||||||
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
types "k8s.io/apimachinery/pkg/types"
|
||||||
|
watch "k8s.io/apimachinery/pkg/watch"
|
||||||
|
rest "k8s.io/client-go/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StacksGetter has a method to return a StackInterface.
|
||||||
|
// A group's client should implement this interface.
|
||||||
|
type StacksGetter interface {
|
||||||
|
Stacks(namespace string) StackInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
// StackInterface has methods to work with Stack resources.
|
||||||
|
type StackInterface interface {
|
||||||
|
Create(*v1beta1.Stack) (*v1beta1.Stack, error)
|
||||||
|
Update(*v1beta1.Stack) (*v1beta1.Stack, error)
|
||||||
|
UpdateStatus(*v1beta1.Stack) (*v1beta1.Stack, error)
|
||||||
|
Delete(name string, options *v1.DeleteOptions) error
|
||||||
|
DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error
|
||||||
|
Get(name string, options v1.GetOptions) (*v1beta1.Stack, error)
|
||||||
|
List(opts v1.ListOptions) (*v1beta1.StackList, error)
|
||||||
|
Watch(opts v1.ListOptions) (watch.Interface, error)
|
||||||
|
Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta1.Stack, err error)
|
||||||
|
StackExpansion
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ StackInterface = &stacks{}
|
||||||
|
|
||||||
|
// stacks implements StackInterface
|
||||||
|
type stacks struct {
|
||||||
|
client rest.Interface
|
||||||
|
ns string
|
||||||
|
}
|
||||||
|
|
||||||
|
// newStacks returns a Stacks
|
||||||
|
func newStacks(c *ComposeV1beta1Client, namespace string) *stacks {
|
||||||
|
return &stacks{
|
||||||
|
client: c.RESTClient(),
|
||||||
|
ns: namespace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create takes the representation of a stack and creates it. Returns the server's representation of the stack, and an error, if there is any.
|
||||||
|
func (c *stacks) Create(stack *v1beta1.Stack) (result *v1beta1.Stack, err error) {
|
||||||
|
result = &v1beta1.Stack{}
|
||||||
|
err = c.client.Post().
|
||||||
|
Namespace(c.ns).
|
||||||
|
Resource("stacks").
|
||||||
|
Body(stack).
|
||||||
|
Do().
|
||||||
|
Into(result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update takes the representation of a stack and updates it. Returns the server's representation of the stack, and an error, if there is any.
|
||||||
|
func (c *stacks) Update(stack *v1beta1.Stack) (result *v1beta1.Stack, err error) {
|
||||||
|
result = &v1beta1.Stack{}
|
||||||
|
err = c.client.Put().
|
||||||
|
Namespace(c.ns).
|
||||||
|
Resource("stacks").
|
||||||
|
Name(stack.Name).
|
||||||
|
Body(stack).
|
||||||
|
Do().
|
||||||
|
Into(result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStatus was generated because the type contains a Status member.
|
||||||
|
// Add a +genclientstatus=false comment above the type to avoid generating UpdateStatus().
|
||||||
|
|
||||||
|
func (c *stacks) UpdateStatus(stack *v1beta1.Stack) (result *v1beta1.Stack, err error) {
|
||||||
|
result = &v1beta1.Stack{}
|
||||||
|
err = c.client.Put().
|
||||||
|
Namespace(c.ns).
|
||||||
|
Resource("stacks").
|
||||||
|
Name(stack.Name).
|
||||||
|
SubResource("status").
|
||||||
|
Body(stack).
|
||||||
|
Do().
|
||||||
|
Into(result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete takes name of the stack and deletes it. Returns an error if one occurs.
|
||||||
|
func (c *stacks) Delete(name string, options *v1.DeleteOptions) error {
|
||||||
|
return c.client.Delete().
|
||||||
|
Namespace(c.ns).
|
||||||
|
Resource("stacks").
|
||||||
|
Name(name).
|
||||||
|
Body(options).
|
||||||
|
Do().
|
||||||
|
Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCollection deletes a collection of objects.
|
||||||
|
func (c *stacks) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
|
||||||
|
return c.client.Delete().
|
||||||
|
Namespace(c.ns).
|
||||||
|
Resource("stacks").
|
||||||
|
VersionedParams(&listOptions, scheme.ParameterCodec).
|
||||||
|
Body(options).
|
||||||
|
Do().
|
||||||
|
Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get takes name of the stack, and returns the corresponding stack object, and an error if there is any.
|
||||||
|
func (c *stacks) Get(name string, options v1.GetOptions) (result *v1beta1.Stack, err error) {
|
||||||
|
result = &v1beta1.Stack{}
|
||||||
|
err = c.client.Get().
|
||||||
|
Namespace(c.ns).
|
||||||
|
Resource("stacks").
|
||||||
|
Name(name).
|
||||||
|
VersionedParams(&options, scheme.ParameterCodec).
|
||||||
|
Do().
|
||||||
|
Into(result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// List takes label and field selectors, and returns the list of Stacks that match those selectors.
|
||||||
|
func (c *stacks) List(opts v1.ListOptions) (result *v1beta1.StackList, err error) {
|
||||||
|
result = &v1beta1.StackList{}
|
||||||
|
err = c.client.Get().
|
||||||
|
Namespace(c.ns).
|
||||||
|
Resource("stacks").
|
||||||
|
VersionedParams(&opts, scheme.ParameterCodec).
|
||||||
|
Do().
|
||||||
|
Into(result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch returns a watch.Interface that watches the requested stacks.
|
||||||
|
func (c *stacks) Watch(opts v1.ListOptions) (watch.Interface, error) {
|
||||||
|
opts.Watch = true
|
||||||
|
return c.client.Get().
|
||||||
|
Namespace(c.ns).
|
||||||
|
Resource("stacks").
|
||||||
|
VersionedParams(&opts, scheme.ParameterCodec).
|
||||||
|
Watch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch applies the patch and returns the patched stack.
|
||||||
|
func (c *stacks) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta1.Stack, err error) {
|
||||||
|
result = &v1beta1.Stack{}
|
||||||
|
err = c.client.Patch(pt).
|
||||||
|
Namespace(c.ns).
|
||||||
|
Resource("stacks").
|
||||||
|
SubResource(subresources...).
|
||||||
|
Name(name).
|
||||||
|
Body(data).
|
||||||
|
Do().
|
||||||
|
Into(result)
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
// +k8s:deepcopy-gen=package,register
|
||||||
|
// +groupName=compose.docker.com
|
||||||
|
|
||||||
|
// Package compose is the internal version of the API.
|
||||||
|
package compose
|
|
@ -0,0 +1,43 @@
|
||||||
|
package compose
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GroupName is the group name used to register these objects
|
||||||
|
const GroupName = "compose.docker.com"
|
||||||
|
|
||||||
|
// SchemeGroupVersion is group version used to register these objects
|
||||||
|
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}
|
||||||
|
|
||||||
|
// Kind takes an unqualified kind and returns back a Group qualified GroupKind
|
||||||
|
func Kind(kind string) schema.GroupKind {
|
||||||
|
return SchemeGroupVersion.WithKind(kind).GroupKind()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource takes an unqualified resource and returns back a Group qualified GroupResource
|
||||||
|
func Resource(resource string) schema.GroupResource {
|
||||||
|
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// SchemeBuilder collects functions that add things to a scheme. It's to allow
|
||||||
|
// code to compile without explicitly referencing generated types. You should
|
||||||
|
// declare one in each package that will have generated deep copy or conversion
|
||||||
|
// functions.
|
||||||
|
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
|
||||||
|
|
||||||
|
// AddToScheme applies all the stored functions to the scheme. A non-nil error
|
||||||
|
// indicates that one function failed and the attempt was abandoned.
|
||||||
|
AddToScheme = SchemeBuilder.AddToScheme
|
||||||
|
)
|
||||||
|
|
||||||
|
// adds the list of known types to api.Scheme.
|
||||||
|
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||||
|
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||||
|
&Stack{},
|
||||||
|
&StackList{},
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
package compose
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ImpersonationConfig holds information use to impersonate calls from the compose controller
|
||||||
|
type ImpersonationConfig struct {
|
||||||
|
// UserName is the username to impersonate on each request.
|
||||||
|
UserName string
|
||||||
|
// Groups are the groups to impersonate on each request.
|
||||||
|
Groups []string
|
||||||
|
// Extra is a free-form field which can be used to link some authentication information
|
||||||
|
// to authorization information. This field allows you to impersonate it.
|
||||||
|
Extra map[string][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stack defines a stack object to be register in the kubernetes API
|
||||||
|
// +genclient=true
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
type Stack struct {
|
||||||
|
metav1.TypeMeta
|
||||||
|
metav1.ObjectMeta
|
||||||
|
Spec StackSpec
|
||||||
|
Status StackStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// StackStatus defines the observed state of Stack
|
||||||
|
type StackStatus struct {
|
||||||
|
Phase StackPhase
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// StackSpec defines the desired state of Stack
|
||||||
|
type StackSpec struct {
|
||||||
|
ComposeFile string
|
||||||
|
Owner ImpersonationConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// StackPhase defines the status phase in which the stack is.
|
||||||
|
type StackPhase string
|
||||||
|
|
||||||
|
// These are valid conditions of a stack.
|
||||||
|
const (
|
||||||
|
// Available means the stack is available.
|
||||||
|
StackAvailable StackPhase = "Available"
|
||||||
|
// Progressing means the deployment is progressing.
|
||||||
|
StackProgressing StackPhase = "Progressing"
|
||||||
|
// StackFailure is added in a stack when one of its members fails to be created
|
||||||
|
// or deleted.
|
||||||
|
StackFailure StackPhase = "Failure"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StackList defines a list of stacks
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
type StackList struct {
|
||||||
|
metav1.TypeMeta
|
||||||
|
metav1.ListMeta
|
||||||
|
Items []Stack
|
||||||
|
}
|
||||||
|
|
||||||
|
// Owner defines the owner of a stack. It is used to impersonate the controller calls
|
||||||
|
// to kubernetes api.
|
||||||
|
type Owner struct {
|
||||||
|
metav1.TypeMeta
|
||||||
|
metav1.ObjectMeta
|
||||||
|
Owner ImpersonationConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// OwnerList defines a list of owner.
|
||||||
|
type OwnerList struct {
|
||||||
|
metav1.TypeMeta
|
||||||
|
metav1.ListMeta
|
||||||
|
Items []Owner
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME(vdemeester) are those necessary ??
|
||||||
|
|
||||||
|
// NewStatus is newStatus
|
||||||
|
func (Stack) NewStatus() interface{} {
|
||||||
|
return StackStatus{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatus returns the status
|
||||||
|
func (pc *Stack) GetStatus() interface{} {
|
||||||
|
return pc.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStatus sets the status
|
||||||
|
func (pc *Stack) SetStatus(s interface{}) {
|
||||||
|
pc.Status = s.(StackStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSpec returns the spec
|
||||||
|
func (pc *Stack) GetSpec() interface{} {
|
||||||
|
return pc.Spec
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSpec sets the spec
|
||||||
|
func (pc *Stack) SetSpec(s interface{}) {
|
||||||
|
pc.Spec = s.(StackSpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectMeta returns the ObjectMeta
|
||||||
|
func (pc *Stack) GetObjectMeta() *metav1.ObjectMeta {
|
||||||
|
return &pc.ObjectMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGeneration sets the Generation
|
||||||
|
func (pc *Stack) SetGeneration(generation int64) {
|
||||||
|
pc.ObjectMeta.Generation = generation
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGeneration returns the Generation
|
||||||
|
func (pc Stack) GetGeneration() int64 {
|
||||||
|
return pc.ObjectMeta.Generation
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Package v1beta1 holds the v1beta1 versions of our stack structures.
|
||||||
|
// API versions allow the api contract for a resource to be changed while keeping
|
||||||
|
// backward compatibility by support multiple concurrent versions
|
||||||
|
// of the same resource
|
||||||
|
//
|
||||||
|
// +k8s:openapi-gen=true
|
||||||
|
// +k8s:deepcopy-gen=package,register
|
||||||
|
// +k8s:conversion-gen=github.com/docker/cli/kubernetes/compose
|
||||||
|
// +k8s:defaulter-gen=TypeMeta
|
||||||
|
// +groupName=compose.docker.com
|
||||||
|
package v1beta1 // import "github.com/docker/cli/kubernetes/compose/v1beta1"
|
|
@ -0,0 +1,25 @@
|
||||||
|
package v1beta1
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
"github.com/docker/cli/kubernetes/compose"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Owner defines the owner of a stack. It is used to impersonate the controller calls
|
||||||
|
// to kubernetes api.
|
||||||
|
// +genclient=true
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
// +subresource-request
|
||||||
|
type Owner struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||||
|
Owner compose.ImpersonationConfig `json:"owner,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OwnerList defines a list of owner.
|
||||||
|
type OwnerList struct {
|
||||||
|
metav1.TypeMeta
|
||||||
|
metav1.ListMeta
|
||||||
|
Items []Owner
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package v1beta1
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GroupName is the group name used to register these objects
|
||||||
|
const GroupName = "compose.docker.com"
|
||||||
|
|
||||||
|
// SchemeGroupVersion is group version used to register these objects
|
||||||
|
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1beta1"}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// SchemeBuilder collects functions that add things to a scheme. It's to allow
|
||||||
|
// code to compile without explicitly referencing generated types. You should
|
||||||
|
// declare one in each package that will have generated deep copy or conversion
|
||||||
|
// functions.
|
||||||
|
SchemeBuilder runtime.SchemeBuilder
|
||||||
|
localSchemeBuilder = &SchemeBuilder
|
||||||
|
|
||||||
|
// AddToScheme applies all the stored functions to the scheme. A non-nil error
|
||||||
|
// indicates that one function failed and the attempt was abandoned.
|
||||||
|
AddToScheme = localSchemeBuilder.AddToScheme
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
localSchemeBuilder.Register(addKnownTypes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds the list of known types to api.Scheme.
|
||||||
|
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||||
|
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||||
|
&Stack{},
|
||||||
|
&StackList{},
|
||||||
|
&Owner{},
|
||||||
|
&OwnerList{},
|
||||||
|
)
|
||||||
|
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource takes an unqualified resource and returns a Group qualified GroupResource
|
||||||
|
func Resource(resource string) schema.GroupResource {
|
||||||
|
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package v1beta1
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StackList defines a list of stacks
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
type StackList struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
|
||||||
|
|
||||||
|
Items []Stack `json:"items" protobuf:"bytes,2,rep,name=items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +genclient=true
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
|
||||||
|
// Stack defines a stack object to be register in the kubernetes API
|
||||||
|
// +k8s:openapi-gen=true
|
||||||
|
// +resource:path=stacks,strategy=StackStrategy
|
||||||
|
// +subresource:request=Owner,path=owner,rest=OwnerStackREST
|
||||||
|
type Stack struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||||
|
|
||||||
|
Spec StackSpec `json:"spec,omitempty"`
|
||||||
|
Status StackStatus `json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StackSpec defines the desired state of Stack
|
||||||
|
type StackSpec struct {
|
||||||
|
ComposeFile string `json:"composeFile,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StackPhase defines the status phase in which the stack is.
|
||||||
|
type StackPhase string
|
||||||
|
|
||||||
|
// These are valid conditions of a stack.
|
||||||
|
const (
|
||||||
|
// Available means the stack is available.
|
||||||
|
StackAvailable StackPhase = "Available"
|
||||||
|
// Progressing means the deployment is progressing.
|
||||||
|
StackProgressing StackPhase = "Progressing"
|
||||||
|
// StackFailure is added in a stack when one of its members fails to be created
|
||||||
|
// or deleted.
|
||||||
|
StackFailure StackPhase = "Failure"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StackStatus defines the observed state of Stack
|
||||||
|
type StackStatus struct {
|
||||||
|
// Current condition of the stack.
|
||||||
|
// +optional
|
||||||
|
Phase StackPhase `json:"phase,omitempty" protobuf:"bytes,1,opt,name=phase,casttype=StackPhase"`
|
||||||
|
// A human readable message indicating details about the stack.
|
||||||
|
// +optional
|
||||||
|
Message string `json:"message,omitempty" protobuf:"bytes,5,opt,name=message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone implements the Cloner interface for kubernetes
|
||||||
|
func (s *Stack) Clone() (*Stack, error) {
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
if err := AddToScheme(scheme); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s.DeepCopy(), nil
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
version: "3.2"
|
||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
image: nginx
|
||||||
|
deploy:
|
||||||
|
replicas: 2
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
|
@ -0,0 +1,10 @@
|
||||||
|
version: "3.2"
|
||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
image: nginx
|
||||||
|
deploy:
|
||||||
|
replicas: 2
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
deploy:
|
||||||
|
replicas: 5
|
|
@ -0,0 +1,10 @@
|
||||||
|
version: "3.2"
|
||||||
|
services:
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 64M
|
||||||
|
reservations:
|
||||||
|
memory: 64M
|
|
@ -0,0 +1,11 @@
|
||||||
|
version: "3.2"
|
||||||
|
services:
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 64M
|
||||||
|
reservations:
|
||||||
|
memory: 64M
|
||||||
|
replicas: 5
|
|
@ -0,0 +1,6 @@
|
||||||
|
version: "3.2"
|
||||||
|
services:
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
deploy:
|
||||||
|
replicas: 2
|
|
@ -0,0 +1,6 @@
|
||||||
|
version: "3.2"
|
||||||
|
services:
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
deploy:
|
||||||
|
replicas: 5
|
|
@ -0,0 +1,4 @@
|
||||||
|
version: "3.2"
|
||||||
|
services:
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
|
@ -0,0 +1,6 @@
|
||||||
|
version: "3.2"
|
||||||
|
services:
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
deploy:
|
||||||
|
replicas: 5
|
|
@ -0,0 +1,169 @@
|
||||||
|
// +build !ignore_autogenerated
|
||||||
|
|
||||||
|
// This file was autogenerated by conversion-gen. Do not edit it manually!
|
||||||
|
|
||||||
|
package v1beta1
|
||||||
|
|
||||||
|
import (
|
||||||
|
compose "github.com/docker/cli/kubernetes/compose"
|
||||||
|
conversion "k8s.io/apimachinery/pkg/conversion"
|
||||||
|
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
localSchemeBuilder.Register(RegisterConversions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterConversions adds conversion functions to the given scheme.
|
||||||
|
// Public to allow building arbitrary schemes.
|
||||||
|
func RegisterConversions(scheme *runtime.Scheme) error {
|
||||||
|
return scheme.AddGeneratedConversionFuncs(
|
||||||
|
Convert_v1beta1_Owner_To_compose_Owner,
|
||||||
|
Convert_compose_Owner_To_v1beta1_Owner,
|
||||||
|
Convert_v1beta1_Stack_To_compose_Stack,
|
||||||
|
Convert_compose_Stack_To_v1beta1_Stack,
|
||||||
|
Convert_v1beta1_StackList_To_compose_StackList,
|
||||||
|
Convert_compose_StackList_To_v1beta1_StackList,
|
||||||
|
Convert_v1beta1_StackSpec_To_compose_StackSpec,
|
||||||
|
Convert_compose_StackSpec_To_v1beta1_StackSpec,
|
||||||
|
Convert_v1beta1_StackStatus_To_compose_StackStatus,
|
||||||
|
Convert_compose_StackStatus_To_v1beta1_StackStatus,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func autoConvert_v1beta1_Owner_To_compose_Owner(in *Owner, out *compose.Owner, s conversion.Scope) error {
|
||||||
|
out.ObjectMeta = in.ObjectMeta
|
||||||
|
out.Owner = in.Owner
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert_v1beta1_Owner_To_compose_Owner is an autogenerated conversion function.
|
||||||
|
func Convert_v1beta1_Owner_To_compose_Owner(in *Owner, out *compose.Owner, s conversion.Scope) error {
|
||||||
|
return autoConvert_v1beta1_Owner_To_compose_Owner(in, out, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func autoConvert_compose_Owner_To_v1beta1_Owner(in *compose.Owner, out *Owner, s conversion.Scope) error {
|
||||||
|
out.ObjectMeta = in.ObjectMeta
|
||||||
|
out.Owner = in.Owner
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert_compose_Owner_To_v1beta1_Owner is an autogenerated conversion function.
|
||||||
|
func Convert_compose_Owner_To_v1beta1_Owner(in *compose.Owner, out *Owner, s conversion.Scope) error {
|
||||||
|
return autoConvert_compose_Owner_To_v1beta1_Owner(in, out, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func autoConvert_v1beta1_Stack_To_compose_Stack(in *Stack, out *compose.Stack, s conversion.Scope) error {
|
||||||
|
out.ObjectMeta = in.ObjectMeta
|
||||||
|
if err := Convert_v1beta1_StackSpec_To_compose_StackSpec(&in.Spec, &out.Spec, s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := Convert_v1beta1_StackStatus_To_compose_StackStatus(&in.Status, &out.Status, s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert_v1beta1_Stack_To_compose_Stack is an autogenerated conversion function.
|
||||||
|
func Convert_v1beta1_Stack_To_compose_Stack(in *Stack, out *compose.Stack, s conversion.Scope) error {
|
||||||
|
return autoConvert_v1beta1_Stack_To_compose_Stack(in, out, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func autoConvert_compose_Stack_To_v1beta1_Stack(in *compose.Stack, out *Stack, s conversion.Scope) error {
|
||||||
|
out.ObjectMeta = in.ObjectMeta
|
||||||
|
if err := Convert_compose_StackSpec_To_v1beta1_StackSpec(&in.Spec, &out.Spec, s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := Convert_compose_StackStatus_To_v1beta1_StackStatus(&in.Status, &out.Status, s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert_compose_Stack_To_v1beta1_Stack is an autogenerated conversion function.
|
||||||
|
func Convert_compose_Stack_To_v1beta1_Stack(in *compose.Stack, out *Stack, s conversion.Scope) error {
|
||||||
|
return autoConvert_compose_Stack_To_v1beta1_Stack(in, out, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func autoConvert_v1beta1_StackList_To_compose_StackList(in *StackList, out *compose.StackList, s conversion.Scope) error {
|
||||||
|
out.ListMeta = in.ListMeta
|
||||||
|
if in.Items != nil {
|
||||||
|
in, out := &in.Items, &out.Items
|
||||||
|
*out = make([]compose.Stack, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
if err := Convert_v1beta1_Stack_To_compose_Stack(&(*in)[i], &(*out)[i], s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.Items = nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert_v1beta1_StackList_To_compose_StackList is an autogenerated conversion function.
|
||||||
|
func Convert_v1beta1_StackList_To_compose_StackList(in *StackList, out *compose.StackList, s conversion.Scope) error {
|
||||||
|
return autoConvert_v1beta1_StackList_To_compose_StackList(in, out, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func autoConvert_compose_StackList_To_v1beta1_StackList(in *compose.StackList, out *StackList, s conversion.Scope) error {
|
||||||
|
out.ListMeta = in.ListMeta
|
||||||
|
if in.Items != nil {
|
||||||
|
in, out := &in.Items, &out.Items
|
||||||
|
*out = make([]Stack, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
if err := Convert_compose_Stack_To_v1beta1_Stack(&(*in)[i], &(*out)[i], s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.Items = make([]Stack, 0)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert_compose_StackList_To_v1beta1_StackList is an autogenerated conversion function.
|
||||||
|
func Convert_compose_StackList_To_v1beta1_StackList(in *compose.StackList, out *StackList, s conversion.Scope) error {
|
||||||
|
return autoConvert_compose_StackList_To_v1beta1_StackList(in, out, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func autoConvert_v1beta1_StackSpec_To_compose_StackSpec(in *StackSpec, out *compose.StackSpec, s conversion.Scope) error {
|
||||||
|
out.ComposeFile = in.ComposeFile
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Convert_compose_StackSpec_To_v1beta1_StackSpec(in *compose.StackSpec, out *StackSpec, s conversion.Scope) error {
|
||||||
|
return autoConvert_compose_StackSpec_To_v1beta1_StackSpec(in, out, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert_v1beta1_StackSpec_To_compose_StackSpec is an autogenerated conversion function.
|
||||||
|
func Convert_v1beta1_StackSpec_To_compose_StackSpec(in *StackSpec, out *compose.StackSpec, s conversion.Scope) error {
|
||||||
|
return autoConvert_v1beta1_StackSpec_To_compose_StackSpec(in, out, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func autoConvert_compose_StackSpec_To_v1beta1_StackSpec(in *compose.StackSpec, out *StackSpec, s conversion.Scope) error {
|
||||||
|
out.ComposeFile = in.ComposeFile
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func autoConvert_v1beta1_StackStatus_To_compose_StackStatus(in *StackStatus, out *compose.StackStatus, s conversion.Scope) error {
|
||||||
|
out.Phase = compose.StackPhase(in.Phase)
|
||||||
|
out.Message = in.Message
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert_v1beta1_StackStatus_To_compose_StackStatus is an autogenerated conversion function.
|
||||||
|
func Convert_v1beta1_StackStatus_To_compose_StackStatus(in *StackStatus, out *compose.StackStatus, s conversion.Scope) error {
|
||||||
|
return autoConvert_v1beta1_StackStatus_To_compose_StackStatus(in, out, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func autoConvert_compose_StackStatus_To_v1beta1_StackStatus(in *compose.StackStatus, out *StackStatus, s conversion.Scope) error {
|
||||||
|
out.Phase = StackPhase(in.Phase)
|
||||||
|
out.Message = in.Message
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert_compose_StackStatus_To_v1beta1_StackStatus is an autogenerated conversion function.
|
||||||
|
func Convert_compose_StackStatus_To_v1beta1_StackStatus(in *compose.StackStatus, out *StackStatus, s conversion.Scope) error {
|
||||||
|
return autoConvert_compose_StackStatus_To_v1beta1_StackStatus(in, out, s)
|
||||||
|
}
|
|
@ -0,0 +1,204 @@
|
||||||
|
// +build !ignore_autogenerated
|
||||||
|
|
||||||
|
// This file was autogenerated by deepcopy-gen. Do not edit it manually!
|
||||||
|
|
||||||
|
package v1beta1
|
||||||
|
|
||||||
|
import (
|
||||||
|
conversion "k8s.io/apimachinery/pkg/conversion"
|
||||||
|
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||||
|
reflect "reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Deprecated: register deep-copy functions.
|
||||||
|
func init() {
|
||||||
|
SchemeBuilder.Register(RegisterDeepCopies)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: RegisterDeepCopies adds deep-copy functions to the given scheme. Public
|
||||||
|
// to allow building arbitrary schemes.
|
||||||
|
func RegisterDeepCopies(scheme *runtime.Scheme) error {
|
||||||
|
return scheme.AddGeneratedDeepCopyFuncs(
|
||||||
|
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||||
|
in.(*Owner).DeepCopyInto(out.(*Owner))
|
||||||
|
return nil
|
||||||
|
}, InType: reflect.TypeOf(&Owner{})},
|
||||||
|
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||||
|
in.(*OwnerList).DeepCopyInto(out.(*OwnerList))
|
||||||
|
return nil
|
||||||
|
}, InType: reflect.TypeOf(&OwnerList{})},
|
||||||
|
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||||
|
in.(*Stack).DeepCopyInto(out.(*Stack))
|
||||||
|
return nil
|
||||||
|
}, InType: reflect.TypeOf(&Stack{})},
|
||||||
|
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||||
|
in.(*StackList).DeepCopyInto(out.(*StackList))
|
||||||
|
return nil
|
||||||
|
}, InType: reflect.TypeOf(&StackList{})},
|
||||||
|
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||||
|
in.(*StackSpec).DeepCopyInto(out.(*StackSpec))
|
||||||
|
return nil
|
||||||
|
}, InType: reflect.TypeOf(&StackSpec{})},
|
||||||
|
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||||
|
in.(*StackStatus).DeepCopyInto(out.(*StackStatus))
|
||||||
|
return nil
|
||||||
|
}, InType: reflect.TypeOf(&StackStatus{})},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *Owner) DeepCopyInto(out *Owner) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||||
|
in.Owner.DeepCopyInto(&out.Owner)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new Owner.
|
||||||
|
func (x *Owner) DeepCopy() *Owner {
|
||||||
|
if x == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(Owner)
|
||||||
|
x.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (x *Owner) DeepCopyObject() runtime.Object {
|
||||||
|
if c := x.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *OwnerList) DeepCopyInto(out *OwnerList) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
out.ListMeta = in.ListMeta
|
||||||
|
if in.Items != nil {
|
||||||
|
in, out := &in.Items, &out.Items
|
||||||
|
*out = make([]Owner, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new OwnerList.
|
||||||
|
func (x *OwnerList) DeepCopy() *OwnerList {
|
||||||
|
if x == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(OwnerList)
|
||||||
|
x.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (x *OwnerList) DeepCopyObject() runtime.Object {
|
||||||
|
if c := x.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *Stack) DeepCopyInto(out *Stack) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||||
|
out.Spec = in.Spec
|
||||||
|
out.Status = in.Status
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new Stack.
|
||||||
|
func (x *Stack) DeepCopy() *Stack {
|
||||||
|
if x == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(Stack)
|
||||||
|
x.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (x *Stack) DeepCopyObject() runtime.Object {
|
||||||
|
if c := x.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *StackList) DeepCopyInto(out *StackList) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
out.ListMeta = in.ListMeta
|
||||||
|
if in.Items != nil {
|
||||||
|
in, out := &in.Items, &out.Items
|
||||||
|
*out = make([]Stack, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new StackList.
|
||||||
|
func (x *StackList) DeepCopy() *StackList {
|
||||||
|
if x == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(StackList)
|
||||||
|
x.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (x *StackList) DeepCopyObject() runtime.Object {
|
||||||
|
if c := x.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *StackSpec) DeepCopyInto(out *StackSpec) {
|
||||||
|
*out = *in
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new StackSpec.
|
||||||
|
func (x *StackSpec) DeepCopy() *StackSpec {
|
||||||
|
if x == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(StackSpec)
|
||||||
|
x.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *StackStatus) DeepCopyInto(out *StackStatus) {
|
||||||
|
*out = *in
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new StackStatus.
|
||||||
|
func (x *StackStatus) DeepCopy() *StackStatus {
|
||||||
|
if x == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(StackStatus)
|
||||||
|
x.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
|
@ -0,0 +1,231 @@
|
||||||
|
// +build !ignore_autogenerated
|
||||||
|
|
||||||
|
// This file was autogenerated by deepcopy-gen. Do not edit it manually!
|
||||||
|
|
||||||
|
package compose
|
||||||
|
|
||||||
|
import (
|
||||||
|
conversion "k8s.io/apimachinery/pkg/conversion"
|
||||||
|
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||||
|
reflect "reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Deprecated: register deep-copy functions.
|
||||||
|
func init() {
|
||||||
|
SchemeBuilder.Register(RegisterDeepCopies)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: RegisterDeepCopies adds deep-copy functions to the given scheme. Public
|
||||||
|
// to allow building arbitrary schemes.
|
||||||
|
func RegisterDeepCopies(scheme *runtime.Scheme) error {
|
||||||
|
return scheme.AddGeneratedDeepCopyFuncs(
|
||||||
|
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||||
|
in.(*ImpersonationConfig).DeepCopyInto(out.(*ImpersonationConfig))
|
||||||
|
return nil
|
||||||
|
}, InType: reflect.TypeOf(&ImpersonationConfig{})},
|
||||||
|
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||||
|
in.(*Owner).DeepCopyInto(out.(*Owner))
|
||||||
|
return nil
|
||||||
|
}, InType: reflect.TypeOf(&Owner{})},
|
||||||
|
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||||
|
in.(*OwnerList).DeepCopyInto(out.(*OwnerList))
|
||||||
|
return nil
|
||||||
|
}, InType: reflect.TypeOf(&OwnerList{})},
|
||||||
|
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||||
|
in.(*Stack).DeepCopyInto(out.(*Stack))
|
||||||
|
return nil
|
||||||
|
}, InType: reflect.TypeOf(&Stack{})},
|
||||||
|
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||||
|
in.(*StackList).DeepCopyInto(out.(*StackList))
|
||||||
|
return nil
|
||||||
|
}, InType: reflect.TypeOf(&StackList{})},
|
||||||
|
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||||
|
in.(*StackSpec).DeepCopyInto(out.(*StackSpec))
|
||||||
|
return nil
|
||||||
|
}, InType: reflect.TypeOf(&StackSpec{})},
|
||||||
|
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||||
|
in.(*StackStatus).DeepCopyInto(out.(*StackStatus))
|
||||||
|
return nil
|
||||||
|
}, InType: reflect.TypeOf(&StackStatus{})},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *ImpersonationConfig) DeepCopyInto(out *ImpersonationConfig) {
|
||||||
|
*out = *in
|
||||||
|
if in.Groups != nil {
|
||||||
|
in, out := &in.Groups, &out.Groups
|
||||||
|
*out = make([]string, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
if in.Extra != nil {
|
||||||
|
in, out := &in.Extra, &out.Extra
|
||||||
|
*out = make(map[string][]string, len(*in))
|
||||||
|
for key, val := range *in {
|
||||||
|
if val == nil {
|
||||||
|
(*out)[key] = nil
|
||||||
|
} else {
|
||||||
|
(*out)[key] = make([]string, len(val))
|
||||||
|
copy((*out)[key], val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationConfig.
|
||||||
|
func (x *ImpersonationConfig) DeepCopy() *ImpersonationConfig {
|
||||||
|
if x == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(ImpersonationConfig)
|
||||||
|
x.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *Owner) DeepCopyInto(out *Owner) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||||
|
in.Owner.DeepCopyInto(&out.Owner)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new Owner.
|
||||||
|
func (x *Owner) DeepCopy() *Owner {
|
||||||
|
if x == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(Owner)
|
||||||
|
x.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Owner) DeepCopyObject() runtime.Object {
|
||||||
|
if c := x.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *OwnerList) DeepCopyInto(out *OwnerList) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
out.ListMeta = in.ListMeta
|
||||||
|
if in.Items != nil {
|
||||||
|
in, out := &in.Items, &out.Items
|
||||||
|
*out = make([]Owner, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new OwnerList.
|
||||||
|
func (x *OwnerList) DeepCopy() *OwnerList {
|
||||||
|
if x == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(OwnerList)
|
||||||
|
x.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *Stack) DeepCopyInto(out *Stack) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||||
|
in.Spec.DeepCopyInto(&out.Spec)
|
||||||
|
out.Status = in.Status
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new Stack.
|
||||||
|
func (x *Stack) DeepCopy() *Stack {
|
||||||
|
if x == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(Stack)
|
||||||
|
x.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (x *Stack) DeepCopyObject() runtime.Object {
|
||||||
|
if c := x.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *StackList) DeepCopyInto(out *StackList) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
out.ListMeta = in.ListMeta
|
||||||
|
if in.Items != nil {
|
||||||
|
in, out := &in.Items, &out.Items
|
||||||
|
*out = make([]Stack, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new StackList.
|
||||||
|
func (x *StackList) DeepCopy() *StackList {
|
||||||
|
if x == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(StackList)
|
||||||
|
x.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (x *StackList) DeepCopyObject() runtime.Object {
|
||||||
|
if c := x.DeepCopy(); c != nil {
|
||||||
|
return c
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *StackSpec) DeepCopyInto(out *StackSpec) {
|
||||||
|
*out = *in
|
||||||
|
in.Owner.DeepCopyInto(&out.Owner)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new StackSpec.
|
||||||
|
func (x *StackSpec) DeepCopy() *StackSpec {
|
||||||
|
if x == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(StackSpec)
|
||||||
|
x.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *StackStatus) DeepCopyInto(out *StackStatus) {
|
||||||
|
*out = *in
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, creating a new StackStatus.
|
||||||
|
func (x *StackStatus) DeepCopy() *StackStatus {
|
||||||
|
if x == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(StackStatus)
|
||||||
|
x.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
//
|
||||||
|
// +domain=docker.com
|
||||||
|
|
||||||
|
package kubernetes
|
|
@ -0,0 +1,58 @@
|
||||||
|
package labels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ForServiceName is the label for the service name.
|
||||||
|
ForServiceName = "com.docker.service.name"
|
||||||
|
// ForStackName is the label for the stack name.
|
||||||
|
ForStackName = "com.docker.stack.namespace"
|
||||||
|
// ForServiceID is the label for the service id.
|
||||||
|
ForServiceID = "com.docker.service.id"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ForService gives the labels to select a given service in a stack.
|
||||||
|
func ForService(stackName, serviceName string) map[string]string {
|
||||||
|
labels := map[string]string{}
|
||||||
|
|
||||||
|
if serviceName != "" {
|
||||||
|
labels[ForServiceName] = serviceName
|
||||||
|
}
|
||||||
|
if stackName != "" {
|
||||||
|
labels[ForStackName] = stackName
|
||||||
|
}
|
||||||
|
if serviceName != "" && stackName != "" {
|
||||||
|
labels[ForServiceID] = stackName + "-" + serviceName
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge merges multiple lists of labels.
|
||||||
|
func Merge(labelsList ...map[string]string) map[string]string {
|
||||||
|
merged := map[string]string{}
|
||||||
|
|
||||||
|
for _, labels := range labelsList {
|
||||||
|
for k, v := range labels {
|
||||||
|
merged[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectorForStack gives the labelSelector to use for a given stack.
|
||||||
|
// Specific service names can be passed to narrow down the selection.
|
||||||
|
func SelectorForStack(stackName string, serviceNames ...string) string {
|
||||||
|
switch len(serviceNames) {
|
||||||
|
case 0:
|
||||||
|
return fmt.Sprintf("%s=%s", ForStackName, stackName)
|
||||||
|
case 1:
|
||||||
|
return fmt.Sprintf("%s=%s,%s=%s", ForStackName, stackName, ForServiceName, serviceNames[0])
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%s=%s,%s in (%s)", ForStackName, stackName, ForServiceName, strings.Join(serviceNames, ","))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package labels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestForService(t *testing.T) {
|
||||||
|
labels := ForService("stack", "service")
|
||||||
|
|
||||||
|
assert.Len(t, labels, 3)
|
||||||
|
assert.Equal(t, "stack", labels["com.docker.stack.namespace"])
|
||||||
|
assert.Equal(t, "service", labels["com.docker.service.name"])
|
||||||
|
assert.Equal(t, "stack-service", labels["com.docker.service.id"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectorForStack(t *testing.T) {
|
||||||
|
assert.Equal(t, "com.docker.stack.namespace=demostack", SelectorForStack("demostack"))
|
||||||
|
assert.Equal(t, "com.docker.stack.namespace=stack,com.docker.service.name=service", SelectorForStack("stack", "service"))
|
||||||
|
assert.Equal(t, "com.docker.stack.namespace=stack,com.docker.service.name in (service1,service2)", SelectorForStack("stack", "service1", "service2"))
|
||||||
|
}
|
34
vendor.conf
34
vendor.conf
|
@ -7,7 +7,6 @@ github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76
|
||||||
github.com/docker/distribution edc3ab29cdff8694dd6feb85cfeb4b5f1b38ed9c
|
github.com/docker/distribution edc3ab29cdff8694dd6feb85cfeb4b5f1b38ed9c
|
||||||
github.com/docker/docker a1be987ea9e03e5ebdb1b415a7acdd8d6f0aaa08
|
github.com/docker/docker a1be987ea9e03e5ebdb1b415a7acdd8d6f0aaa08
|
||||||
github.com/docker/docker-credential-helpers 3c90bd29a46b943b2a9842987b58fb91a7c1819b
|
github.com/docker/docker-credential-helpers 3c90bd29a46b943b2a9842987b58fb91a7c1819b
|
||||||
|
|
||||||
# the docker/go package contains a customized version of canonical/json
|
# the docker/go package contains a customized version of canonical/json
|
||||||
# and is used by Notary. The package is periodically rebased on current Go versions.
|
# and is used by Notary. The package is periodically rebased on current Go versions.
|
||||||
github.com/docker/go d30aec9fd63c35133f8f79c3412ad91a3b08be06
|
github.com/docker/go d30aec9fd63c35133f8f79c3412ad91a3b08be06
|
||||||
|
@ -15,13 +14,33 @@ github.com/docker/go-connections 3ede32e2033de7505e6500d6c868c2b9ed9f169d
|
||||||
github.com/docker/go-events 9461782956ad83b30282bf90e31fa6a70c255ba9
|
github.com/docker/go-events 9461782956ad83b30282bf90e31fa6a70c255ba9
|
||||||
github.com/docker/go-units 9e638d38cf6977a37a8ea0078f3ee75a7cdb2dd1
|
github.com/docker/go-units 9e638d38cf6977a37a8ea0078f3ee75a7cdb2dd1
|
||||||
github.com/docker/swarmkit de950a7ed842c7b7e47e9451cde9bf8f96031894
|
github.com/docker/swarmkit de950a7ed842c7b7e47e9451cde9bf8f96031894
|
||||||
|
github.com/emicklei/go-restful ff4f55a206334ef123e4f79bbf348980da81ca46
|
||||||
|
github.com/emicklei/go-restful-swagger12 dcef7f55730566d41eae5db10e7d6981829720f6
|
||||||
github.com/flynn-archive/go-shlex 3f9db97f856818214da2e1057f8ad84803971cff
|
github.com/flynn-archive/go-shlex 3f9db97f856818214da2e1057f8ad84803971cff
|
||||||
|
github.com/ghodss/yaml 0ca9ea5df5451ffdf184b4428c902747c2c11cd7
|
||||||
github.com/gogo/protobuf v0.4
|
github.com/gogo/protobuf v0.4
|
||||||
|
github.com/golang/glog 44145f04b68cf362d9c4df2182967c2275eaefed
|
||||||
github.com/golang/protobuf 7a211bcf3bce0e3f1d74f9894916e6f116ae83b4
|
github.com/golang/protobuf 7a211bcf3bce0e3f1d74f9894916e6f116ae83b4
|
||||||
|
github.com/google/btree 316fb6d3f031ae8f4d457c6c5186b9e3ded70435
|
||||||
|
github.com/google/gofuzz 44d81051d367757e1c7c6a5a86423ece9afcf63c
|
||||||
|
github.com/googleapis/gnostic e4f56557df6250e1945ee6854f181ce4e1c2c646
|
||||||
github.com/gorilla/context v1.1
|
github.com/gorilla/context v1.1
|
||||||
github.com/gorilla/mux v1.1
|
github.com/gorilla/mux v1.1
|
||||||
github.com/gotestyourself/gotestyourself v1.2.0
|
github.com/gotestyourself/gotestyourself v1.2.0
|
||||||
|
# FIXME(vdemeester) try to deduplicate this with gojsonpointer
|
||||||
|
github.com/go-openapi/jsonpointer 46af16f9f7b149af66e5d1bd010e3574dc06de98
|
||||||
|
# FIXME(vdemeester) try to deduplicate this with gojsonreference
|
||||||
|
github.com/go-openapi/jsonreference 13c6e3589ad90f49bd3e3bbe2c2cb3d7a4142272
|
||||||
|
github.com/go-openapi/spec 6aced65f8501fe1217321abf0749d354824ba2ff
|
||||||
|
github.com/go-openapi/swag 1d0bd113de87027671077d3c71eb3ac5d7dbba72
|
||||||
|
github.com/gregjones/httpcache c1f8028e62adb3d518b823a2f8e6a95c38bdd3aa
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway 1a03ca3bad1e1ebadaedd3abb76bc58d4ac8143b
|
||||||
|
github.com/howeyc/gopass 3ca23474a7c7203e0a0a070fd33508f6efdb9b3d
|
||||||
|
github.com/imdario/mergo 6633656539c1639d9d78127b7d47c622b5d7b6dc
|
||||||
github.com/inconshreveable/mousetrap 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
|
github.com/inconshreveable/mousetrap 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
|
||||||
|
github.com/juju/ratelimit 5b9ff866471762aa2ab2dced63c9fb6f53921342
|
||||||
|
github.com/json-iterator/go 6240e1e7983a85228f7fd9c3e1b6932d46ec58e2
|
||||||
|
github.com/mailru/easyjson d5b7844b561a7bc640052f1b935f7b800330d7e0
|
||||||
github.com/mattn/go-shellwords v1.0.3
|
github.com/mattn/go-shellwords v1.0.3
|
||||||
github.com/Microsoft/go-winio v0.4.5
|
github.com/Microsoft/go-winio v0.4.5
|
||||||
github.com/miekg/pkcs11 df8ae6ca730422dba20c768ff38ef7d79077a59f
|
github.com/miekg/pkcs11 df8ae6ca730422dba20c768ff38ef7d79077a59f
|
||||||
|
@ -31,8 +50,11 @@ github.com/Nvveen/Gotty a8b993ba6abdb0e0c12b0125c603323a71c7790c https://github.
|
||||||
github.com/opencontainers/go-digest 21dfd564fd89c944783d00d069f33e3e7123c448
|
github.com/opencontainers/go-digest 21dfd564fd89c944783d00d069f33e3e7123c448
|
||||||
github.com/opencontainers/image-spec v1.0.0
|
github.com/opencontainers/image-spec v1.0.0
|
||||||
github.com/opencontainers/runc b2567b37d7b75eb4cf325b77297b140ea686ce8f
|
github.com/opencontainers/runc b2567b37d7b75eb4cf325b77297b140ea686ce8f
|
||||||
|
github.com/peterbourgon/diskv 5f041e8faa004a95c88a202771f4cc3e991971e6
|
||||||
github.com/pkg/errors 839d9e913e063e28dfd0e6c7b7512793e0a48be9
|
github.com/pkg/errors 839d9e913e063e28dfd0e6c7b7512793e0a48be9
|
||||||
github.com/pmezard/go-difflib v1.0.0
|
github.com/pmezard/go-difflib v1.0.0
|
||||||
|
github.com/PuerkitoBio/purell 8a290539e2e8629dbc4e6bad948158f790ec31f4
|
||||||
|
github.com/PuerkitoBio/urlesc 5bd2802263f21d8788851d5305584c82a5c75d7e
|
||||||
github.com/russross/blackfriday 1d6b8e9301e720b08a8938b8c25c018285885438
|
github.com/russross/blackfriday 1d6b8e9301e720b08a8938b8c25c018285885438
|
||||||
github.com/shurcooL/sanitized_anchor_name 10ef21a441db47d8b13ebcc5fd2310f636973c77
|
github.com/shurcooL/sanitized_anchor_name 10ef21a441db47d8b13ebcc5fd2310f636973c77
|
||||||
github.com/sirupsen/logrus v1.0.3
|
github.com/sirupsen/logrus v1.0.3
|
||||||
|
@ -45,12 +67,18 @@ github.com/xeipuuv/gojsonpointer e0fe6f68307607d540ed8eac07a342c33fa1b54a
|
||||||
github.com/xeipuuv/gojsonreference e02fc20de94c78484cd5ffb007f8af96be030a45
|
github.com/xeipuuv/gojsonreference e02fc20de94c78484cd5ffb007f8af96be030a45
|
||||||
github.com/xeipuuv/gojsonschema 93e72a773fade158921402d6a24c819b48aba29d
|
github.com/xeipuuv/gojsonschema 93e72a773fade158921402d6a24c819b48aba29d
|
||||||
golang.org/x/crypto 558b6879de74bc843225cde5686419267ff707ca
|
golang.org/x/crypto 558b6879de74bc843225cde5686419267ff707ca
|
||||||
golang.org/x/net 7dcfb8076726a3fdd9353b6b8a1f1b6be6811bd6
|
golang.org/x/net a8b9294777976932365dabb6640cf1468d95c70f
|
||||||
golang.org/x/sync 450f422ab23cf9881c94e2db30cac0eb1b7cf80c
|
|
||||||
golang.org/x/sys 95c6576299259db960f6c5b9b69ea52422860fce
|
golang.org/x/sys 95c6576299259db960f6c5b9b69ea52422860fce
|
||||||
golang.org/x/text f72d8390a633d5dfb0cc84043294db9f6c935756
|
golang.org/x/text f72d8390a633d5dfb0cc84043294db9f6c935756
|
||||||
golang.org/x/time a4bde12657593d5e90d0533a3e4fd95e635124cb
|
golang.org/x/time a4bde12657593d5e90d0533a3e4fd95e635124cb
|
||||||
google.golang.org/genproto d80a6e20e776b0b17a324d0ba1ab50a39c8e8944
|
google.golang.org/genproto d80a6e20e776b0b17a324d0ba1ab50a39c8e8944
|
||||||
google.golang.org/grpc v1.3.0
|
google.golang.org/grpc v1.3.0
|
||||||
|
gopkg.in/inf.v0 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4
|
||||||
gopkg.in/yaml.v2 4c78c975fe7c825c6d1466c42be594d1d6f3aba6
|
gopkg.in/yaml.v2 4c78c975fe7c825c6d1466c42be594d1d6f3aba6
|
||||||
|
k8s.io/api kubernetes-1.8.2
|
||||||
|
k8s.io/apimachinery kubernetes-1.8.2
|
||||||
|
k8s.io/client-go kubernetes-1.8.2
|
||||||
|
k8s.io/kubernetes v1.8.2
|
||||||
|
k8s.io/kube-openapi 61b46af70dfed79c6d24530cd23b41440a7f22a5
|
||||||
vbom.ml/util 928aaa586d7718c70f4090ddf83f2b34c16fdc8d
|
vbom.ml/util 928aaa586d7718c70f4090ddf83f2b34c16fdc8d
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
Copyright (c) 2012, Martin Angers
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,185 @@
|
||||||
|
# Purell
|
||||||
|
|
||||||
|
Purell is a tiny Go library to normalize URLs. It returns a pure URL. Pure-ell. Sanitizer and all. Yeah, I know...
|
||||||
|
|
||||||
|
Based on the [wikipedia paper][wiki] and the [RFC 3986 document][rfc].
|
||||||
|
|
||||||
|
[![build status](https://secure.travis-ci.org/PuerkitoBio/purell.png)](http://travis-ci.org/PuerkitoBio/purell)
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
`go get github.com/PuerkitoBio/purell`
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
* **2016-07-27 (v1.0.0)** : Normalize IDN to ASCII (thanks to @zenovich).
|
||||||
|
* **2015-02-08** : Add fix for relative paths issue ([PR #5][pr5]) and add fix for unnecessary encoding of reserved characters ([see issue #7][iss7]).
|
||||||
|
* **v0.2.0** : Add benchmarks, Attempt IDN support.
|
||||||
|
* **v0.1.0** : Initial release.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
From `example_test.go` (note that in your code, you would import "github.com/PuerkitoBio/purell", and would prefix references to its methods and constants with "purell."):
|
||||||
|
|
||||||
|
```go
|
||||||
|
package purell
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleNormalizeURLString() {
|
||||||
|
if normalized, err := NormalizeURLString("hTTp://someWEBsite.com:80/Amazing%3f/url/",
|
||||||
|
FlagLowercaseScheme|FlagLowercaseHost|FlagUppercaseEscapes); err != nil {
|
||||||
|
panic(err)
|
||||||
|
} else {
|
||||||
|
fmt.Print(normalized)
|
||||||
|
}
|
||||||
|
// Output: http://somewebsite.com:80/Amazing%3F/url/
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleMustNormalizeURLString() {
|
||||||
|
normalized := MustNormalizeURLString("hTTpS://someWEBsite.com:443/Amazing%fa/url/",
|
||||||
|
FlagsUnsafeGreedy)
|
||||||
|
fmt.Print(normalized)
|
||||||
|
|
||||||
|
// Output: http://somewebsite.com/Amazing%FA/url
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleNormalizeURL() {
|
||||||
|
if u, err := url.Parse("Http://SomeUrl.com:8080/a/b/.././c///g?c=3&a=1&b=9&c=0#target"); err != nil {
|
||||||
|
panic(err)
|
||||||
|
} else {
|
||||||
|
normalized := NormalizeURL(u, FlagsUsuallySafeGreedy|FlagRemoveDuplicateSlashes|FlagRemoveFragment)
|
||||||
|
fmt.Print(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output: http://someurl.com:8080/a/c/g?c=3&a=1&b=9&c=0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
As seen in the examples above, purell offers three methods, `NormalizeURLString(string, NormalizationFlags) (string, error)`, `MustNormalizeURLString(string, NormalizationFlags) (string)` and `NormalizeURL(*url.URL, NormalizationFlags) (string)`. They all normalize the provided URL based on the specified flags. Here are the available flags:
|
||||||
|
|
||||||
|
```go
|
||||||
|
const (
|
||||||
|
// Safe normalizations
|
||||||
|
FlagLowercaseScheme NormalizationFlags = 1 << iota // HTTP://host -> http://host, applied by default in Go1.1
|
||||||
|
FlagLowercaseHost // http://HOST -> http://host
|
||||||
|
FlagUppercaseEscapes // http://host/t%ef -> http://host/t%EF
|
||||||
|
FlagDecodeUnnecessaryEscapes // http://host/t%41 -> http://host/tA
|
||||||
|
FlagEncodeNecessaryEscapes // http://host/!"#$ -> http://host/%21%22#$
|
||||||
|
FlagRemoveDefaultPort // http://host:80 -> http://host
|
||||||
|
FlagRemoveEmptyQuerySeparator // http://host/path? -> http://host/path
|
||||||
|
|
||||||
|
// Usually safe normalizations
|
||||||
|
FlagRemoveTrailingSlash // http://host/path/ -> http://host/path
|
||||||
|
FlagAddTrailingSlash // http://host/path -> http://host/path/ (should choose only one of these add/remove trailing slash flags)
|
||||||
|
FlagRemoveDotSegments // http://host/path/./a/b/../c -> http://host/path/a/c
|
||||||
|
|
||||||
|
// Unsafe normalizations
|
||||||
|
FlagRemoveDirectoryIndex // http://host/path/index.html -> http://host/path/
|
||||||
|
FlagRemoveFragment // http://host/path#fragment -> http://host/path
|
||||||
|
FlagForceHTTP // https://host -> http://host
|
||||||
|
FlagRemoveDuplicateSlashes // http://host/path//a///b -> http://host/path/a/b
|
||||||
|
FlagRemoveWWW // http://www.host/ -> http://host/
|
||||||
|
FlagAddWWW // http://host/ -> http://www.host/ (should choose only one of these add/remove WWW flags)
|
||||||
|
FlagSortQuery // http://host/path?c=3&b=2&a=1&b=1 -> http://host/path?a=1&b=1&b=2&c=3
|
||||||
|
|
||||||
|
// Normalizations not in the wikipedia article, required to cover tests cases
|
||||||
|
// submitted by jehiah
|
||||||
|
FlagDecodeDWORDHost // http://1113982867 -> http://66.102.7.147
|
||||||
|
FlagDecodeOctalHost // http://0102.0146.07.0223 -> http://66.102.7.147
|
||||||
|
FlagDecodeHexHost // http://0x42660793 -> http://66.102.7.147
|
||||||
|
FlagRemoveUnnecessaryHostDots // http://.host../path -> http://host/path
|
||||||
|
FlagRemoveEmptyPortSeparator // http://host:/path -> http://host/path
|
||||||
|
|
||||||
|
// Convenience set of safe normalizations
|
||||||
|
FlagsSafe NormalizationFlags = FlagLowercaseHost | FlagLowercaseScheme | FlagUppercaseEscapes | FlagDecodeUnnecessaryEscapes | FlagEncodeNecessaryEscapes | FlagRemoveDefaultPort | FlagRemoveEmptyQuerySeparator
|
||||||
|
|
||||||
|
// For convenience sets, "greedy" uses the "remove trailing slash" and "remove www. prefix" flags,
|
||||||
|
// while "non-greedy" uses the "add (or keep) the trailing slash" and "add www. prefix".
|
||||||
|
|
||||||
|
// Convenience set of usually safe normalizations (includes FlagsSafe)
|
||||||
|
FlagsUsuallySafeGreedy NormalizationFlags = FlagsSafe | FlagRemoveTrailingSlash | FlagRemoveDotSegments
|
||||||
|
FlagsUsuallySafeNonGreedy NormalizationFlags = FlagsSafe | FlagAddTrailingSlash | FlagRemoveDotSegments
|
||||||
|
|
||||||
|
// Convenience set of unsafe normalizations (includes FlagsUsuallySafe)
|
||||||
|
FlagsUnsafeGreedy NormalizationFlags = FlagsUsuallySafeGreedy | FlagRemoveDirectoryIndex | FlagRemoveFragment | FlagForceHTTP | FlagRemoveDuplicateSlashes | FlagRemoveWWW | FlagSortQuery
|
||||||
|
FlagsUnsafeNonGreedy NormalizationFlags = FlagsUsuallySafeNonGreedy | FlagRemoveDirectoryIndex | FlagRemoveFragment | FlagForceHTTP | FlagRemoveDuplicateSlashes | FlagAddWWW | FlagSortQuery
|
||||||
|
|
||||||
|
// Convenience set of all available flags
|
||||||
|
FlagsAllGreedy = FlagsUnsafeGreedy | FlagDecodeDWORDHost | FlagDecodeOctalHost | FlagDecodeHexHost | FlagRemoveUnnecessaryHostDots | FlagRemoveEmptyPortSeparator
|
||||||
|
FlagsAllNonGreedy = FlagsUnsafeNonGreedy | FlagDecodeDWORDHost | FlagDecodeOctalHost | FlagDecodeHexHost | FlagRemoveUnnecessaryHostDots | FlagRemoveEmptyPortSeparator
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
For convenience, the set of flags `FlagsSafe`, `FlagsUsuallySafe[Greedy|NonGreedy]`, `FlagsUnsafe[Greedy|NonGreedy]` and `FlagsAll[Greedy|NonGreedy]` are provided for the similarly grouped normalizations on [wikipedia's URL normalization page][wiki]. You can add (using the bitwise OR `|` operator) or remove (using the bitwise AND NOT `&^` operator) individual flags from the sets if required, to build your own custom set.
|
||||||
|
|
||||||
|
The [full godoc reference is available on gopkgdoc][godoc].
|
||||||
|
|
||||||
|
Some things to note:
|
||||||
|
|
||||||
|
* `FlagDecodeUnnecessaryEscapes`, `FlagEncodeNecessaryEscapes`, `FlagUppercaseEscapes` and `FlagRemoveEmptyQuerySeparator` are always implicitly set, because internally, the URL string is parsed as an URL object, which automatically decodes unnecessary escapes, uppercases and encodes necessary ones, and removes empty query separators (an unnecessary `?` at the end of the url). So this operation cannot **not** be done. For this reason, `FlagRemoveEmptyQuerySeparator` (as well as the other three) has been included in the `FlagsSafe` convenience set, instead of `FlagsUnsafe`, where Wikipedia puts it.
|
||||||
|
|
||||||
|
* The `FlagDecodeUnnecessaryEscapes` decodes the following escapes (*from -> to*):
|
||||||
|
- %24 -> $
|
||||||
|
- %26 -> &
|
||||||
|
- %2B-%3B -> +,-./0123456789:;
|
||||||
|
- %3D -> =
|
||||||
|
- %40-%5A -> @ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
|
- %5F -> _
|
||||||
|
- %61-%7A -> abcdefghijklmnopqrstuvwxyz
|
||||||
|
- %7E -> ~
|
||||||
|
|
||||||
|
|
||||||
|
* When the `NormalizeURL` function is used (passing an URL object), this source URL object is modified (that is, after the call, the URL object will be modified to reflect the normalization).
|
||||||
|
|
||||||
|
* The *replace IP with domain name* normalization (`http://208.77.188.166/ → http://www.example.com/`) is obviously not possible for a library without making some network requests. This is not implemented in purell.
|
||||||
|
|
||||||
|
* The *remove unused query string parameters* and *remove default query parameters* are also not implemented, since this is a very case-specific normalization, and it is quite trivial to do with an URL object.
|
||||||
|
|
||||||
|
### Safe vs Usually Safe vs Unsafe
|
||||||
|
|
||||||
|
Purell allows you to control the level of risk you take while normalizing an URL. You can aggressively normalize, play it totally safe, or anything in between.
|
||||||
|
|
||||||
|
Consider the following URL:
|
||||||
|
|
||||||
|
`HTTPS://www.RooT.com/toto/t%45%1f///a/./b/../c/?z=3&w=2&a=4&w=1#invalid`
|
||||||
|
|
||||||
|
Normalizing with the `FlagsSafe` gives:
|
||||||
|
|
||||||
|
`https://www.root.com/toto/tE%1F///a/./b/../c/?z=3&w=2&a=4&w=1#invalid`
|
||||||
|
|
||||||
|
With the `FlagsUsuallySafeGreedy`:
|
||||||
|
|
||||||
|
`https://www.root.com/toto/tE%1F///a/c?z=3&w=2&a=4&w=1#invalid`
|
||||||
|
|
||||||
|
And with `FlagsUnsafeGreedy`:
|
||||||
|
|
||||||
|
`http://root.com/toto/tE%1F/a/c?a=4&w=1&w=2&z=3`
|
||||||
|
|
||||||
|
## TODOs
|
||||||
|
|
||||||
|
* Add a class/default instance to allow specifying custom directory index names? At the moment, removing directory index removes `(^|/)((?:default|index)\.\w{1,4})$`.
|
||||||
|
|
||||||
|
## Thanks / Contributions
|
||||||
|
|
||||||
|
@rogpeppe
|
||||||
|
@jehiah
|
||||||
|
@opennota
|
||||||
|
@pchristopher1275
|
||||||
|
@zenovich
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The [BSD 3-Clause license][bsd].
|
||||||
|
|
||||||
|
[bsd]: http://opensource.org/licenses/BSD-3-Clause
|
||||||
|
[wiki]: http://en.wikipedia.org/wiki/URL_normalization
|
||||||
|
[rfc]: http://tools.ietf.org/html/rfc3986#section-6
|
||||||
|
[godoc]: http://go.pkgdoc.org/github.com/PuerkitoBio/purell
|
||||||
|
[pr5]: https://github.com/PuerkitoBio/purell/pull/5
|
||||||
|
[iss7]: https://github.com/PuerkitoBio/purell/issues/7
|
|
@ -0,0 +1,375 @@
|
||||||
|
/*
|
||||||
|
Package purell offers URL normalization as described on the wikipedia page:
|
||||||
|
http://en.wikipedia.org/wiki/URL_normalization
|
||||||
|
*/
|
||||||
|
package purell
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/urlesc"
|
||||||
|
"golang.org/x/net/idna"
|
||||||
|
"golang.org/x/text/secure/precis"
|
||||||
|
"golang.org/x/text/unicode/norm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A set of normalization flags determines how a URL will
|
||||||
|
// be normalized.
|
||||||
|
type NormalizationFlags uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Safe normalizations
|
||||||
|
FlagLowercaseScheme NormalizationFlags = 1 << iota // HTTP://host -> http://host, applied by default in Go1.1
|
||||||
|
FlagLowercaseHost // http://HOST -> http://host
|
||||||
|
FlagUppercaseEscapes // http://host/t%ef -> http://host/t%EF
|
||||||
|
FlagDecodeUnnecessaryEscapes // http://host/t%41 -> http://host/tA
|
||||||
|
FlagEncodeNecessaryEscapes // http://host/!"#$ -> http://host/%21%22#$
|
||||||
|
FlagRemoveDefaultPort // http://host:80 -> http://host
|
||||||
|
FlagRemoveEmptyQuerySeparator // http://host/path? -> http://host/path
|
||||||
|
|
||||||
|
// Usually safe normalizations
|
||||||
|
FlagRemoveTrailingSlash // http://host/path/ -> http://host/path
|
||||||
|
FlagAddTrailingSlash // http://host/path -> http://host/path/ (should choose only one of these add/remove trailing slash flags)
|
||||||
|
FlagRemoveDotSegments // http://host/path/./a/b/../c -> http://host/path/a/c
|
||||||
|
|
||||||
|
// Unsafe normalizations
|
||||||
|
FlagRemoveDirectoryIndex // http://host/path/index.html -> http://host/path/
|
||||||
|
FlagRemoveFragment // http://host/path#fragment -> http://host/path
|
||||||
|
FlagForceHTTP // https://host -> http://host
|
||||||
|
FlagRemoveDuplicateSlashes // http://host/path//a///b -> http://host/path/a/b
|
||||||
|
FlagRemoveWWW // http://www.host/ -> http://host/
|
||||||
|
FlagAddWWW // http://host/ -> http://www.host/ (should choose only one of these add/remove WWW flags)
|
||||||
|
FlagSortQuery // http://host/path?c=3&b=2&a=1&b=1 -> http://host/path?a=1&b=1&b=2&c=3
|
||||||
|
|
||||||
|
// Normalizations not in the wikipedia article, required to cover tests cases
|
||||||
|
// submitted by jehiah
|
||||||
|
FlagDecodeDWORDHost // http://1113982867 -> http://66.102.7.147
|
||||||
|
FlagDecodeOctalHost // http://0102.0146.07.0223 -> http://66.102.7.147
|
||||||
|
FlagDecodeHexHost // http://0x42660793 -> http://66.102.7.147
|
||||||
|
FlagRemoveUnnecessaryHostDots // http://.host../path -> http://host/path
|
||||||
|
FlagRemoveEmptyPortSeparator // http://host:/path -> http://host/path
|
||||||
|
|
||||||
|
// Convenience set of safe normalizations
|
||||||
|
FlagsSafe NormalizationFlags = FlagLowercaseHost | FlagLowercaseScheme | FlagUppercaseEscapes | FlagDecodeUnnecessaryEscapes | FlagEncodeNecessaryEscapes | FlagRemoveDefaultPort | FlagRemoveEmptyQuerySeparator
|
||||||
|
|
||||||
|
// For convenience sets, "greedy" uses the "remove trailing slash" and "remove www. prefix" flags,
|
||||||
|
// while "non-greedy" uses the "add (or keep) the trailing slash" and "add www. prefix".
|
||||||
|
|
||||||
|
// Convenience set of usually safe normalizations (includes FlagsSafe)
|
||||||
|
FlagsUsuallySafeGreedy NormalizationFlags = FlagsSafe | FlagRemoveTrailingSlash | FlagRemoveDotSegments
|
||||||
|
FlagsUsuallySafeNonGreedy NormalizationFlags = FlagsSafe | FlagAddTrailingSlash | FlagRemoveDotSegments
|
||||||
|
|
||||||
|
// Convenience set of unsafe normalizations (includes FlagsUsuallySafe)
|
||||||
|
FlagsUnsafeGreedy NormalizationFlags = FlagsUsuallySafeGreedy | FlagRemoveDirectoryIndex | FlagRemoveFragment | FlagForceHTTP | FlagRemoveDuplicateSlashes | FlagRemoveWWW | FlagSortQuery
|
||||||
|
FlagsUnsafeNonGreedy NormalizationFlags = FlagsUsuallySafeNonGreedy | FlagRemoveDirectoryIndex | FlagRemoveFragment | FlagForceHTTP | FlagRemoveDuplicateSlashes | FlagAddWWW | FlagSortQuery
|
||||||
|
|
||||||
|
// Convenience set of all available flags
|
||||||
|
FlagsAllGreedy = FlagsUnsafeGreedy | FlagDecodeDWORDHost | FlagDecodeOctalHost | FlagDecodeHexHost | FlagRemoveUnnecessaryHostDots | FlagRemoveEmptyPortSeparator
|
||||||
|
FlagsAllNonGreedy = FlagsUnsafeNonGreedy | FlagDecodeDWORDHost | FlagDecodeOctalHost | FlagDecodeHexHost | FlagRemoveUnnecessaryHostDots | FlagRemoveEmptyPortSeparator
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultHttpPort = ":80"
|
||||||
|
defaultHttpsPort = ":443"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Regular expressions used by the normalizations
|
||||||
|
var rxPort = regexp.MustCompile(`(:\d+)/?$`)
|
||||||
|
var rxDirIndex = regexp.MustCompile(`(^|/)((?:default|index)\.\w{1,4})$`)
|
||||||
|
var rxDupSlashes = regexp.MustCompile(`/{2,}`)
|
||||||
|
var rxDWORDHost = regexp.MustCompile(`^(\d+)((?:\.+)?(?:\:\d*)?)$`)
|
||||||
|
var rxOctalHost = regexp.MustCompile(`^(0\d*)\.(0\d*)\.(0\d*)\.(0\d*)((?:\.+)?(?:\:\d*)?)$`)
|
||||||
|
var rxHexHost = regexp.MustCompile(`^0x([0-9A-Fa-f]+)((?:\.+)?(?:\:\d*)?)$`)
|
||||||
|
var rxHostDots = regexp.MustCompile(`^(.+?)(:\d+)?$`)
|
||||||
|
var rxEmptyPort = regexp.MustCompile(`:+$`)
|
||||||
|
|
||||||
|
// Map of flags to implementation function.
|
||||||
|
// FlagDecodeUnnecessaryEscapes has no action, since it is done automatically
|
||||||
|
// by parsing the string as an URL. Same for FlagUppercaseEscapes and FlagRemoveEmptyQuerySeparator.
|
||||||
|
|
||||||
|
// Since maps have undefined traversing order, make a slice of ordered keys
|
||||||
|
var flagsOrder = []NormalizationFlags{
|
||||||
|
FlagLowercaseScheme,
|
||||||
|
FlagLowercaseHost,
|
||||||
|
FlagRemoveDefaultPort,
|
||||||
|
FlagRemoveDirectoryIndex,
|
||||||
|
FlagRemoveDotSegments,
|
||||||
|
FlagRemoveFragment,
|
||||||
|
FlagForceHTTP, // Must be after remove default port (because https=443/http=80)
|
||||||
|
FlagRemoveDuplicateSlashes,
|
||||||
|
FlagRemoveWWW,
|
||||||
|
FlagAddWWW,
|
||||||
|
FlagSortQuery,
|
||||||
|
FlagDecodeDWORDHost,
|
||||||
|
FlagDecodeOctalHost,
|
||||||
|
FlagDecodeHexHost,
|
||||||
|
FlagRemoveUnnecessaryHostDots,
|
||||||
|
FlagRemoveEmptyPortSeparator,
|
||||||
|
FlagRemoveTrailingSlash, // These two (add/remove trailing slash) must be last
|
||||||
|
FlagAddTrailingSlash,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... and then the map, where order is unimportant
|
||||||
|
var flags = map[NormalizationFlags]func(*url.URL){
|
||||||
|
FlagLowercaseScheme: lowercaseScheme,
|
||||||
|
FlagLowercaseHost: lowercaseHost,
|
||||||
|
FlagRemoveDefaultPort: removeDefaultPort,
|
||||||
|
FlagRemoveDirectoryIndex: removeDirectoryIndex,
|
||||||
|
FlagRemoveDotSegments: removeDotSegments,
|
||||||
|
FlagRemoveFragment: removeFragment,
|
||||||
|
FlagForceHTTP: forceHTTP,
|
||||||
|
FlagRemoveDuplicateSlashes: removeDuplicateSlashes,
|
||||||
|
FlagRemoveWWW: removeWWW,
|
||||||
|
FlagAddWWW: addWWW,
|
||||||
|
FlagSortQuery: sortQuery,
|
||||||
|
FlagDecodeDWORDHost: decodeDWORDHost,
|
||||||
|
FlagDecodeOctalHost: decodeOctalHost,
|
||||||
|
FlagDecodeHexHost: decodeHexHost,
|
||||||
|
FlagRemoveUnnecessaryHostDots: removeUnncessaryHostDots,
|
||||||
|
FlagRemoveEmptyPortSeparator: removeEmptyPortSeparator,
|
||||||
|
FlagRemoveTrailingSlash: removeTrailingSlash,
|
||||||
|
FlagAddTrailingSlash: addTrailingSlash,
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustNormalizeURLString returns the normalized string, and panics if an error occurs.
|
||||||
|
// It takes an URL string as input, as well as the normalization flags.
|
||||||
|
func MustNormalizeURLString(u string, f NormalizationFlags) string {
|
||||||
|
result, e := NormalizeURLString(u, f)
|
||||||
|
if e != nil {
|
||||||
|
panic(e)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeURLString returns the normalized string, or an error if it can't be parsed into an URL object.
|
||||||
|
// It takes an URL string as input, as well as the normalization flags.
|
||||||
|
func NormalizeURLString(u string, f NormalizationFlags) (string, error) {
|
||||||
|
if parsed, e := url.Parse(u); e != nil {
|
||||||
|
return "", e
|
||||||
|
} else {
|
||||||
|
options := make([]precis.Option, 1, 3)
|
||||||
|
options[0] = precis.IgnoreCase
|
||||||
|
if f&FlagLowercaseHost == FlagLowercaseHost {
|
||||||
|
options = append(options, precis.FoldCase())
|
||||||
|
}
|
||||||
|
options = append(options, precis.Norm(norm.NFC))
|
||||||
|
profile := precis.NewFreeform(options...)
|
||||||
|
if parsed.Host, e = idna.ToASCII(profile.NewTransformer().String(parsed.Host)); e != nil {
|
||||||
|
return "", e
|
||||||
|
}
|
||||||
|
return NormalizeURL(parsed, f), nil
|
||||||
|
}
|
||||||
|
panic("Unreachable code.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeURL returns the normalized string.
|
||||||
|
// It takes a parsed URL object as input, as well as the normalization flags.
|
||||||
|
func NormalizeURL(u *url.URL, f NormalizationFlags) string {
|
||||||
|
for _, k := range flagsOrder {
|
||||||
|
if f&k == k {
|
||||||
|
flags[k](u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urlesc.Escape(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lowercaseScheme(u *url.URL) {
|
||||||
|
if len(u.Scheme) > 0 {
|
||||||
|
u.Scheme = strings.ToLower(u.Scheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lowercaseHost(u *url.URL) {
|
||||||
|
if len(u.Host) > 0 {
|
||||||
|
u.Host = strings.ToLower(u.Host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeDefaultPort(u *url.URL) {
|
||||||
|
if len(u.Host) > 0 {
|
||||||
|
scheme := strings.ToLower(u.Scheme)
|
||||||
|
u.Host = rxPort.ReplaceAllStringFunc(u.Host, func(val string) string {
|
||||||
|
if (scheme == "http" && val == defaultHttpPort) || (scheme == "https" && val == defaultHttpsPort) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeTrailingSlash(u *url.URL) {
|
||||||
|
if l := len(u.Path); l > 0 {
|
||||||
|
if strings.HasSuffix(u.Path, "/") {
|
||||||
|
u.Path = u.Path[:l-1]
|
||||||
|
}
|
||||||
|
} else if l = len(u.Host); l > 0 {
|
||||||
|
if strings.HasSuffix(u.Host, "/") {
|
||||||
|
u.Host = u.Host[:l-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addTrailingSlash(u *url.URL) {
|
||||||
|
if l := len(u.Path); l > 0 {
|
||||||
|
if !strings.HasSuffix(u.Path, "/") {
|
||||||
|
u.Path += "/"
|
||||||
|
}
|
||||||
|
} else if l = len(u.Host); l > 0 {
|
||||||
|
if !strings.HasSuffix(u.Host, "/") {
|
||||||
|
u.Host += "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeDotSegments(u *url.URL) {
|
||||||
|
if len(u.Path) > 0 {
|
||||||
|
var dotFree []string
|
||||||
|
var lastIsDot bool
|
||||||
|
|
||||||
|
sections := strings.Split(u.Path, "/")
|
||||||
|
for _, s := range sections {
|
||||||
|
if s == ".." {
|
||||||
|
if len(dotFree) > 0 {
|
||||||
|
dotFree = dotFree[:len(dotFree)-1]
|
||||||
|
}
|
||||||
|
} else if s != "." {
|
||||||
|
dotFree = append(dotFree, s)
|
||||||
|
}
|
||||||
|
lastIsDot = (s == "." || s == "..")
|
||||||
|
}
|
||||||
|
// Special case if host does not end with / and new path does not begin with /
|
||||||
|
u.Path = strings.Join(dotFree, "/")
|
||||||
|
if u.Host != "" && !strings.HasSuffix(u.Host, "/") && !strings.HasPrefix(u.Path, "/") {
|
||||||
|
u.Path = "/" + u.Path
|
||||||
|
}
|
||||||
|
// Special case if the last segment was a dot, make sure the path ends with a slash
|
||||||
|
if lastIsDot && !strings.HasSuffix(u.Path, "/") {
|
||||||
|
u.Path += "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeDirectoryIndex(u *url.URL) {
|
||||||
|
if len(u.Path) > 0 {
|
||||||
|
u.Path = rxDirIndex.ReplaceAllString(u.Path, "$1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeFragment(u *url.URL) {
|
||||||
|
u.Fragment = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func forceHTTP(u *url.URL) {
|
||||||
|
if strings.ToLower(u.Scheme) == "https" {
|
||||||
|
u.Scheme = "http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeDuplicateSlashes(u *url.URL) {
|
||||||
|
if len(u.Path) > 0 {
|
||||||
|
u.Path = rxDupSlashes.ReplaceAllString(u.Path, "/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeWWW(u *url.URL) {
|
||||||
|
if len(u.Host) > 0 && strings.HasPrefix(strings.ToLower(u.Host), "www.") {
|
||||||
|
u.Host = u.Host[4:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addWWW(u *url.URL) {
|
||||||
|
if len(u.Host) > 0 && !strings.HasPrefix(strings.ToLower(u.Host), "www.") {
|
||||||
|
u.Host = "www." + u.Host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortQuery(u *url.URL) {
|
||||||
|
q := u.Query()
|
||||||
|
|
||||||
|
if len(q) > 0 {
|
||||||
|
arKeys := make([]string, len(q))
|
||||||
|
i := 0
|
||||||
|
for k, _ := range q {
|
||||||
|
arKeys[i] = k
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
sort.Strings(arKeys)
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
for _, k := range arKeys {
|
||||||
|
sort.Strings(q[k])
|
||||||
|
for _, v := range q[k] {
|
||||||
|
if buf.Len() > 0 {
|
||||||
|
buf.WriteRune('&')
|
||||||
|
}
|
||||||
|
buf.WriteString(fmt.Sprintf("%s=%s", k, urlesc.QueryEscape(v)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild the raw query string
|
||||||
|
u.RawQuery = buf.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeDWORDHost(u *url.URL) {
|
||||||
|
if len(u.Host) > 0 {
|
||||||
|
if matches := rxDWORDHost.FindStringSubmatch(u.Host); len(matches) > 2 {
|
||||||
|
var parts [4]int64
|
||||||
|
|
||||||
|
dword, _ := strconv.ParseInt(matches[1], 10, 0)
|
||||||
|
for i, shift := range []uint{24, 16, 8, 0} {
|
||||||
|
parts[i] = dword >> shift & 0xFF
|
||||||
|
}
|
||||||
|
u.Host = fmt.Sprintf("%d.%d.%d.%d%s", parts[0], parts[1], parts[2], parts[3], matches[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeOctalHost(u *url.URL) {
|
||||||
|
if len(u.Host) > 0 {
|
||||||
|
if matches := rxOctalHost.FindStringSubmatch(u.Host); len(matches) > 5 {
|
||||||
|
var parts [4]int64
|
||||||
|
|
||||||
|
for i := 1; i <= 4; i++ {
|
||||||
|
parts[i-1], _ = strconv.ParseInt(matches[i], 8, 0)
|
||||||
|
}
|
||||||
|
u.Host = fmt.Sprintf("%d.%d.%d.%d%s", parts[0], parts[1], parts[2], parts[3], matches[5])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeHexHost(u *url.URL) {
|
||||||
|
if len(u.Host) > 0 {
|
||||||
|
if matches := rxHexHost.FindStringSubmatch(u.Host); len(matches) > 2 {
|
||||||
|
// Conversion is safe because of regex validation
|
||||||
|
parsed, _ := strconv.ParseInt(matches[1], 16, 0)
|
||||||
|
// Set host as DWORD (base 10) encoded host
|
||||||
|
u.Host = fmt.Sprintf("%d%s", parsed, matches[2])
|
||||||
|
// The rest is the same as decoding a DWORD host
|
||||||
|
decodeDWORDHost(u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeUnncessaryHostDots(u *url.URL) {
|
||||||
|
if len(u.Host) > 0 {
|
||||||
|
if matches := rxHostDots.FindStringSubmatch(u.Host); len(matches) > 1 {
|
||||||
|
// Trim the leading and trailing dots
|
||||||
|
u.Host = strings.Trim(matches[1], ".")
|
||||||
|
if len(matches) > 2 {
|
||||||
|
u.Host += matches[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeEmptyPortSeparator(u *url.URL) {
|
||||||
|
if len(u.Host) > 0 {
|
||||||
|
u.Host = rxEmptyPort.ReplaceAllString(u.Host, "")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,16 @@
|
||||||
|
urlesc [![Build Status](https://travis-ci.org/PuerkitoBio/urlesc.png?branch=master)](https://travis-ci.org/PuerkitoBio/urlesc) [![GoDoc](http://godoc.org/github.com/PuerkitoBio/urlesc?status.svg)](http://godoc.org/github.com/PuerkitoBio/urlesc)
|
||||||
|
======
|
||||||
|
|
||||||
|
Package urlesc implements query escaping as per RFC 3986.
|
||||||
|
|
||||||
|
It contains some parts of the net/url package, modified so as to allow
|
||||||
|
some reserved characters incorrectly escaped by net/url (see [issue 5684](https://github.com/golang/go/issues/5684)).
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
go get github.com/PuerkitoBio/urlesc
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Go license (BSD-3-Clause)
|
||||||
|
|
|
@ -0,0 +1,180 @@
|
||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package urlesc implements query escaping as per RFC 3986.
|
||||||
|
// It contains some parts of the net/url package, modified so as to allow
|
||||||
|
// some reserved characters incorrectly escaped by net/url.
|
||||||
|
// See https://github.com/golang/go/issues/5684
|
||||||
|
package urlesc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type encoding int
|
||||||
|
|
||||||
|
const (
|
||||||
|
encodePath encoding = 1 + iota
|
||||||
|
encodeUserPassword
|
||||||
|
encodeQueryComponent
|
||||||
|
encodeFragment
|
||||||
|
)
|
||||||
|
|
||||||
|
// Return true if the specified character should be escaped when
|
||||||
|
// appearing in a URL string, according to RFC 3986.
|
||||||
|
func shouldEscape(c byte, mode encoding) bool {
|
||||||
|
// §2.3 Unreserved characters (alphanum)
|
||||||
|
if 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c {
|
||||||
|
case '-', '.', '_', '~': // §2.3 Unreserved characters (mark)
|
||||||
|
return false
|
||||||
|
|
||||||
|
// §2.2 Reserved characters (reserved)
|
||||||
|
case ':', '/', '?', '#', '[', ']', '@', // gen-delims
|
||||||
|
'!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': // sub-delims
|
||||||
|
// Different sections of the URL allow a few of
|
||||||
|
// the reserved characters to appear unescaped.
|
||||||
|
switch mode {
|
||||||
|
case encodePath: // §3.3
|
||||||
|
// The RFC allows sub-delims and : @.
|
||||||
|
// '/', '[' and ']' can be used to assign meaning to individual path
|
||||||
|
// segments. This package only manipulates the path as a whole,
|
||||||
|
// so we allow those as well. That leaves only ? and # to escape.
|
||||||
|
return c == '?' || c == '#'
|
||||||
|
|
||||||
|
case encodeUserPassword: // §3.2.1
|
||||||
|
// The RFC allows : and sub-delims in
|
||||||
|
// userinfo. The parsing of userinfo treats ':' as special so we must escape
|
||||||
|
// all the gen-delims.
|
||||||
|
return c == ':' || c == '/' || c == '?' || c == '#' || c == '[' || c == ']' || c == '@'
|
||||||
|
|
||||||
|
case encodeQueryComponent: // §3.4
|
||||||
|
// The RFC allows / and ?.
|
||||||
|
return c != '/' && c != '?'
|
||||||
|
|
||||||
|
case encodeFragment: // §4.1
|
||||||
|
// The RFC text is silent but the grammar allows
|
||||||
|
// everything, so escape nothing but #
|
||||||
|
return c == '#'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything else must be escaped.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryEscape escapes the string so it can be safely placed
|
||||||
|
// inside a URL query.
|
||||||
|
func QueryEscape(s string) string {
|
||||||
|
return escape(s, encodeQueryComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func escape(s string, mode encoding) string {
|
||||||
|
spaceCount, hexCount := 0, 0
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
c := s[i]
|
||||||
|
if shouldEscape(c, mode) {
|
||||||
|
if c == ' ' && mode == encodeQueryComponent {
|
||||||
|
spaceCount++
|
||||||
|
} else {
|
||||||
|
hexCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if spaceCount == 0 && hexCount == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
t := make([]byte, len(s)+2*hexCount)
|
||||||
|
j := 0
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
switch c := s[i]; {
|
||||||
|
case c == ' ' && mode == encodeQueryComponent:
|
||||||
|
t[j] = '+'
|
||||||
|
j++
|
||||||
|
case shouldEscape(c, mode):
|
||||||
|
t[j] = '%'
|
||||||
|
t[j+1] = "0123456789ABCDEF"[c>>4]
|
||||||
|
t[j+2] = "0123456789ABCDEF"[c&15]
|
||||||
|
j += 3
|
||||||
|
default:
|
||||||
|
t[j] = s[i]
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
var uiReplacer = strings.NewReplacer(
|
||||||
|
"%21", "!",
|
||||||
|
"%27", "'",
|
||||||
|
"%28", "(",
|
||||||
|
"%29", ")",
|
||||||
|
"%2A", "*",
|
||||||
|
)
|
||||||
|
|
||||||
|
// unescapeUserinfo unescapes some characters that need not to be escaped as per RFC3986.
|
||||||
|
func unescapeUserinfo(s string) string {
|
||||||
|
return uiReplacer.Replace(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape reassembles the URL into a valid URL string.
|
||||||
|
// The general form of the result is one of:
|
||||||
|
//
|
||||||
|
// scheme:opaque
|
||||||
|
// scheme://userinfo@host/path?query#fragment
|
||||||
|
//
|
||||||
|
// If u.Opaque is non-empty, String uses the first form;
|
||||||
|
// otherwise it uses the second form.
|
||||||
|
//
|
||||||
|
// In the second form, the following rules apply:
|
||||||
|
// - if u.Scheme is empty, scheme: is omitted.
|
||||||
|
// - if u.User is nil, userinfo@ is omitted.
|
||||||
|
// - if u.Host is empty, host/ is omitted.
|
||||||
|
// - if u.Scheme and u.Host are empty and u.User is nil,
|
||||||
|
// the entire scheme://userinfo@host/ is omitted.
|
||||||
|
// - if u.Host is non-empty and u.Path begins with a /,
|
||||||
|
// the form host/path does not add its own /.
|
||||||
|
// - if u.RawQuery is empty, ?query is omitted.
|
||||||
|
// - if u.Fragment is empty, #fragment is omitted.
|
||||||
|
func Escape(u *url.URL) string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if u.Scheme != "" {
|
||||||
|
buf.WriteString(u.Scheme)
|
||||||
|
buf.WriteByte(':')
|
||||||
|
}
|
||||||
|
if u.Opaque != "" {
|
||||||
|
buf.WriteString(u.Opaque)
|
||||||
|
} else {
|
||||||
|
if u.Scheme != "" || u.Host != "" || u.User != nil {
|
||||||
|
buf.WriteString("//")
|
||||||
|
if ui := u.User; ui != nil {
|
||||||
|
buf.WriteString(unescapeUserinfo(ui.String()))
|
||||||
|
buf.WriteByte('@')
|
||||||
|
}
|
||||||
|
if h := u.Host; h != "" {
|
||||||
|
buf.WriteString(h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if u.Path != "" && u.Path[0] != '/' && u.Host != "" {
|
||||||
|
buf.WriteByte('/')
|
||||||
|
}
|
||||||
|
buf.WriteString(escape(u.Path, encodePath))
|
||||||
|
}
|
||||||
|
if u.RawQuery != "" {
|
||||||
|
buf.WriteByte('?')
|
||||||
|
buf.WriteString(u.RawQuery)
|
||||||
|
}
|
||||||
|
if u.Fragment != "" {
|
||||||
|
buf.WriteByte('#')
|
||||||
|
buf.WriteString(escape(u.Fragment, encodeFragment))
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
Copyright (c) 2017 Ernest Micklei
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,83 @@
|
||||||
|
# go-restful-swagger12
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/emicklei/go-restful-swagger12.png)](https://travis-ci.org/emicklei/go-restful-swagger12)
|
||||||
|
[![GoDoc](https://godoc.org/github.com/emicklei/go-restful-swagger12?status.svg)](https://godoc.org/github.com/emicklei/go-restful-swagger12)
|
||||||
|
|
||||||
|
How to use Swagger UI with go-restful
|
||||||
|
=
|
||||||
|
|
||||||
|
Get the Swagger UI sources (version 1.2 only)
|
||||||
|
|
||||||
|
git clone https://github.com/wordnik/swagger-ui.git
|
||||||
|
|
||||||
|
The project contains a "dist" folder.
|
||||||
|
Its contents has all the Swagger UI files you need.
|
||||||
|
|
||||||
|
The `index.html` has an `url` set to `http://petstore.swagger.wordnik.com/api/api-docs`.
|
||||||
|
You need to change that to match your WebService JSON endpoint e.g. `http://localhost:8080/apidocs.json`
|
||||||
|
|
||||||
|
Now, you can install the Swagger WebService for serving the Swagger specification in JSON.
|
||||||
|
|
||||||
|
config := swagger.Config{
|
||||||
|
WebServices: restful.RegisteredWebServices(),
|
||||||
|
ApiPath: "/apidocs.json",
|
||||||
|
SwaggerPath: "/apidocs/",
|
||||||
|
SwaggerFilePath: "/Users/emicklei/Projects/swagger-ui/dist"}
|
||||||
|
swagger.InstallSwaggerService(config)
|
||||||
|
|
||||||
|
|
||||||
|
Documenting Structs
|
||||||
|
--
|
||||||
|
|
||||||
|
Currently there are 2 ways to document your structs in the go-restful Swagger.
|
||||||
|
|
||||||
|
###### By using struct tags
|
||||||
|
- Use tag "description" to annotate a struct field with a description to show in the UI
|
||||||
|
- Use tag "modelDescription" to annotate the struct itself with a description to show in the UI. The tag can be added in an field of the struct and in case that there are multiple definition, they will be appended with an empty line.
|
||||||
|
|
||||||
|
###### By using the SwaggerDoc method
|
||||||
|
Here is an example with an `Address` struct and the documentation for each of the fields. The `""` is a special entry for **documenting the struct itself**.
|
||||||
|
|
||||||
|
type Address struct {
|
||||||
|
Country string `json:"country,omitempty"`
|
||||||
|
PostCode int `json:"postcode,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Address) SwaggerDoc() map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"": "Address doc",
|
||||||
|
"country": "Country doc",
|
||||||
|
"postcode": "PostCode doc",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
This example will generate a JSON like this
|
||||||
|
|
||||||
|
{
|
||||||
|
"Address": {
|
||||||
|
"id": "Address",
|
||||||
|
"description": "Address doc",
|
||||||
|
"properties": {
|
||||||
|
"country": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Country doc"
|
||||||
|
},
|
||||||
|
"postcode": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"description": "PostCode doc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
**Very Important Notes:**
|
||||||
|
- `SwaggerDoc()` is using a **NON-Pointer** receiver (e.g. func (Address) and not func (*Address))
|
||||||
|
- The returned map should use as key the name of the field as defined in the JSON parameter (e.g. `"postcode"` and not `"PostCode"`)
|
||||||
|
|
||||||
|
Notes
|
||||||
|
--
|
||||||
|
- The Nickname of an Operation is automatically set by finding the name of the function. You can override it using RouteBuilder.Operation(..)
|
||||||
|
- The WebServices field of swagger.Config can be used to control which service you want to expose and document ; you can have multiple configs and therefore multiple endpoints.
|
||||||
|
|
||||||
|
© 2017, ernestmicklei.com. MIT License. Contributions welcome.
|
64
vendor/github.com/emicklei/go-restful-swagger12/api_declaration_list.go
generated
vendored
Normal file
64
vendor/github.com/emicklei/go-restful-swagger12/api_declaration_list.go
generated
vendored
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package swagger
|
||||||
|
|
||||||
|
// Copyright 2015 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ApiDeclarationList maintains an ordered list of ApiDeclaration.
|
||||||
|
type ApiDeclarationList struct {
|
||||||
|
List []ApiDeclaration
|
||||||
|
}
|
||||||
|
|
||||||
|
// At returns the ApiDeclaration by its path unless absent, then ok is false
|
||||||
|
func (l *ApiDeclarationList) At(path string) (a ApiDeclaration, ok bool) {
|
||||||
|
for _, each := range l.List {
|
||||||
|
if each.ResourcePath == path {
|
||||||
|
return each, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put adds or replaces a ApiDeclaration with this name
|
||||||
|
func (l *ApiDeclarationList) Put(path string, a ApiDeclaration) {
|
||||||
|
// maybe replace existing
|
||||||
|
for i, each := range l.List {
|
||||||
|
if each.ResourcePath == path {
|
||||||
|
// replace
|
||||||
|
l.List[i] = a
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// add
|
||||||
|
l.List = append(l.List, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do enumerates all the properties, each with its assigned name
|
||||||
|
func (l *ApiDeclarationList) Do(block func(path string, decl ApiDeclaration)) {
|
||||||
|
for _, each := range l.List {
|
||||||
|
block(each.ResourcePath, each)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON writes the ModelPropertyList as if it was a map[string]ModelProperty
|
||||||
|
func (l ApiDeclarationList) MarshalJSON() ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
encoder := json.NewEncoder(&buf)
|
||||||
|
buf.WriteString("{\n")
|
||||||
|
for i, each := range l.List {
|
||||||
|
buf.WriteString("\"")
|
||||||
|
buf.WriteString(each.ResourcePath)
|
||||||
|
buf.WriteString("\": ")
|
||||||
|
encoder.Encode(each)
|
||||||
|
if i < len(l.List)-1 {
|
||||||
|
buf.WriteString(",\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.WriteString("}")
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package swagger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/emicklei/go-restful"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PostBuildDeclarationMapFunc can be used to modify the api declaration map.
|
||||||
|
type PostBuildDeclarationMapFunc func(apiDeclarationMap *ApiDeclarationList)
|
||||||
|
|
||||||
|
// MapSchemaFormatFunc can be used to modify typeName at definition time.
|
||||||
|
type MapSchemaFormatFunc func(typeName string) string
|
||||||
|
|
||||||
|
// MapModelTypeNameFunc can be used to return the desired typeName for a given
|
||||||
|
// type. It will return false if the default name should be used.
|
||||||
|
type MapModelTypeNameFunc func(t reflect.Type) (string, bool)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
// url where the services are available, e.g. http://localhost:8080
|
||||||
|
// if left empty then the basePath of Swagger is taken from the actual request
|
||||||
|
WebServicesUrl string
|
||||||
|
// path where the JSON api is avaiable , e.g. /apidocs
|
||||||
|
ApiPath string
|
||||||
|
// [optional] path where the swagger UI will be served, e.g. /swagger
|
||||||
|
SwaggerPath string
|
||||||
|
// [optional] location of folder containing Swagger HTML5 application index.html
|
||||||
|
SwaggerFilePath string
|
||||||
|
// api listing is constructed from this list of restful WebServices.
|
||||||
|
WebServices []*restful.WebService
|
||||||
|
// will serve all static content (scripts,pages,images)
|
||||||
|
StaticHandler http.Handler
|
||||||
|
// [optional] on default CORS (Cross-Origin-Resource-Sharing) is enabled.
|
||||||
|
DisableCORS bool
|
||||||
|
// Top-level API version. Is reflected in the resource listing.
|
||||||
|
ApiVersion string
|
||||||
|
// If set then call this handler after building the complete ApiDeclaration Map
|
||||||
|
PostBuildHandler PostBuildDeclarationMapFunc
|
||||||
|
// Swagger global info struct
|
||||||
|
Info Info
|
||||||
|
// [optional] If set, model builder should call this handler to get addition typename-to-swagger-format-field conversion.
|
||||||
|
SchemaFormatHandler MapSchemaFormatFunc
|
||||||
|
// [optional] If set, model builder should call this handler to retrieve the name for a given type.
|
||||||
|
ModelTypeNameHandler MapModelTypeNameFunc
|
||||||
|
}
|
|
@ -0,0 +1,467 @@
|
||||||
|
package swagger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ModelBuildable is used for extending Structs that need more control over
|
||||||
|
// how the Model appears in the Swagger api declaration.
|
||||||
|
type ModelBuildable interface {
|
||||||
|
PostBuildModel(m *Model) *Model
|
||||||
|
}
|
||||||
|
|
||||||
|
type modelBuilder struct {
|
||||||
|
Models *ModelList
|
||||||
|
Config *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type documentable interface {
|
||||||
|
SwaggerDoc() map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this structure has a method with signature func (<theModel>) SwaggerDoc() map[string]string
|
||||||
|
// If it exists, retrive the documentation and overwrite all struct tag descriptions
|
||||||
|
func getDocFromMethodSwaggerDoc2(model reflect.Type) map[string]string {
|
||||||
|
if docable, ok := reflect.New(model).Elem().Interface().(documentable); ok {
|
||||||
|
return docable.SwaggerDoc()
|
||||||
|
}
|
||||||
|
return make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addModelFrom creates and adds a Model to the builder and detects and calls
|
||||||
|
// the post build hook for customizations
|
||||||
|
func (b modelBuilder) addModelFrom(sample interface{}) {
|
||||||
|
if modelOrNil := b.addModel(reflect.TypeOf(sample), ""); modelOrNil != nil {
|
||||||
|
// allow customizations
|
||||||
|
if buildable, ok := sample.(ModelBuildable); ok {
|
||||||
|
modelOrNil = buildable.PostBuildModel(modelOrNil)
|
||||||
|
b.Models.Put(modelOrNil.Id, *modelOrNil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b modelBuilder) addModel(st reflect.Type, nameOverride string) *Model {
|
||||||
|
// Turn pointers into simpler types so further checks are
|
||||||
|
// correct.
|
||||||
|
if st.Kind() == reflect.Ptr {
|
||||||
|
st = st.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
modelName := b.keyFrom(st)
|
||||||
|
if nameOverride != "" {
|
||||||
|
modelName = nameOverride
|
||||||
|
}
|
||||||
|
// no models needed for primitive types
|
||||||
|
if b.isPrimitiveType(modelName) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// golang encoding/json packages says array and slice values encode as
|
||||||
|
// JSON arrays, except that []byte encodes as a base64-encoded string.
|
||||||
|
// If we see a []byte here, treat it at as a primitive type (string)
|
||||||
|
// and deal with it in buildArrayTypeProperty.
|
||||||
|
if (st.Kind() == reflect.Slice || st.Kind() == reflect.Array) &&
|
||||||
|
st.Elem().Kind() == reflect.Uint8 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// see if we already have visited this model
|
||||||
|
if _, ok := b.Models.At(modelName); ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sm := Model{
|
||||||
|
Id: modelName,
|
||||||
|
Required: []string{},
|
||||||
|
Properties: ModelPropertyList{}}
|
||||||
|
|
||||||
|
// reference the model before further initializing (enables recursive structs)
|
||||||
|
b.Models.Put(modelName, sm)
|
||||||
|
|
||||||
|
// check for slice or array
|
||||||
|
if st.Kind() == reflect.Slice || st.Kind() == reflect.Array {
|
||||||
|
b.addModel(st.Elem(), "")
|
||||||
|
return &sm
|
||||||
|
}
|
||||||
|
// check for structure or primitive type
|
||||||
|
if st.Kind() != reflect.Struct {
|
||||||
|
return &sm
|
||||||
|
}
|
||||||
|
|
||||||
|
fullDoc := getDocFromMethodSwaggerDoc2(st)
|
||||||
|
modelDescriptions := []string{}
|
||||||
|
|
||||||
|
for i := 0; i < st.NumField(); i++ {
|
||||||
|
field := st.Field(i)
|
||||||
|
jsonName, modelDescription, prop := b.buildProperty(field, &sm, modelName)
|
||||||
|
if len(modelDescription) > 0 {
|
||||||
|
modelDescriptions = append(modelDescriptions, modelDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add if not omitted
|
||||||
|
if len(jsonName) != 0 {
|
||||||
|
// update description
|
||||||
|
if fieldDoc, ok := fullDoc[jsonName]; ok {
|
||||||
|
prop.Description = fieldDoc
|
||||||
|
}
|
||||||
|
// update Required
|
||||||
|
if b.isPropertyRequired(field) {
|
||||||
|
sm.Required = append(sm.Required, jsonName)
|
||||||
|
}
|
||||||
|
sm.Properties.Put(jsonName, prop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We always overwrite documentation if SwaggerDoc method exists
|
||||||
|
// "" is special for documenting the struct itself
|
||||||
|
if modelDoc, ok := fullDoc[""]; ok {
|
||||||
|
sm.Description = modelDoc
|
||||||
|
} else if len(modelDescriptions) != 0 {
|
||||||
|
sm.Description = strings.Join(modelDescriptions, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// update model builder with completed model
|
||||||
|
b.Models.Put(modelName, sm)
|
||||||
|
|
||||||
|
return &sm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b modelBuilder) isPropertyRequired(field reflect.StructField) bool {
|
||||||
|
required := true
|
||||||
|
if jsonTag := field.Tag.Get("json"); jsonTag != "" {
|
||||||
|
s := strings.Split(jsonTag, ",")
|
||||||
|
if len(s) > 1 && s[1] == "omitempty" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return required
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b modelBuilder) buildProperty(field reflect.StructField, model *Model, modelName string) (jsonName, modelDescription string, prop ModelProperty) {
|
||||||
|
jsonName = b.jsonNameOfField(field)
|
||||||
|
if len(jsonName) == 0 {
|
||||||
|
// empty name signals skip property
|
||||||
|
return "", "", prop
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Name == "XMLName" && field.Type.String() == "xml.Name" {
|
||||||
|
// property is metadata for the xml.Name attribute, can be skipped
|
||||||
|
return "", "", prop
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag := field.Tag.Get("modelDescription"); tag != "" {
|
||||||
|
modelDescription = tag
|
||||||
|
}
|
||||||
|
|
||||||
|
prop.setPropertyMetadata(field)
|
||||||
|
if prop.Type != nil {
|
||||||
|
return jsonName, modelDescription, prop
|
||||||
|
}
|
||||||
|
fieldType := field.Type
|
||||||
|
|
||||||
|
// check if type is doing its own marshalling
|
||||||
|
marshalerType := reflect.TypeOf((*json.Marshaler)(nil)).Elem()
|
||||||
|
if fieldType.Implements(marshalerType) {
|
||||||
|
var pType = "string"
|
||||||
|
if prop.Type == nil {
|
||||||
|
prop.Type = &pType
|
||||||
|
}
|
||||||
|
if prop.Format == "" {
|
||||||
|
prop.Format = b.jsonSchemaFormat(b.keyFrom(fieldType))
|
||||||
|
}
|
||||||
|
return jsonName, modelDescription, prop
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if annotation says it is a string
|
||||||
|
if jsonTag := field.Tag.Get("json"); jsonTag != "" {
|
||||||
|
s := strings.Split(jsonTag, ",")
|
||||||
|
if len(s) > 1 && s[1] == "string" {
|
||||||
|
stringt := "string"
|
||||||
|
prop.Type = &stringt
|
||||||
|
return jsonName, modelDescription, prop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldKind := fieldType.Kind()
|
||||||
|
switch {
|
||||||
|
case fieldKind == reflect.Struct:
|
||||||
|
jsonName, prop := b.buildStructTypeProperty(field, jsonName, model)
|
||||||
|
return jsonName, modelDescription, prop
|
||||||
|
case fieldKind == reflect.Slice || fieldKind == reflect.Array:
|
||||||
|
jsonName, prop := b.buildArrayTypeProperty(field, jsonName, modelName)
|
||||||
|
return jsonName, modelDescription, prop
|
||||||
|
case fieldKind == reflect.Ptr:
|
||||||
|
jsonName, prop := b.buildPointerTypeProperty(field, jsonName, modelName)
|
||||||
|
return jsonName, modelDescription, prop
|
||||||
|
case fieldKind == reflect.String:
|
||||||
|
stringt := "string"
|
||||||
|
prop.Type = &stringt
|
||||||
|
return jsonName, modelDescription, prop
|
||||||
|
case fieldKind == reflect.Map:
|
||||||
|
// if it's a map, it's unstructured, and swagger 1.2 can't handle it
|
||||||
|
objectType := "object"
|
||||||
|
prop.Type = &objectType
|
||||||
|
return jsonName, modelDescription, prop
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldTypeName := b.keyFrom(fieldType)
|
||||||
|
if b.isPrimitiveType(fieldTypeName) {
|
||||||
|
mapped := b.jsonSchemaType(fieldTypeName)
|
||||||
|
prop.Type = &mapped
|
||||||
|
prop.Format = b.jsonSchemaFormat(fieldTypeName)
|
||||||
|
return jsonName, modelDescription, prop
|
||||||
|
}
|
||||||
|
modelType := b.keyFrom(fieldType)
|
||||||
|
prop.Ref = &modelType
|
||||||
|
|
||||||
|
if fieldType.Name() == "" { // override type of anonymous structs
|
||||||
|
nestedTypeName := modelName + "." + jsonName
|
||||||
|
prop.Ref = &nestedTypeName
|
||||||
|
b.addModel(fieldType, nestedTypeName)
|
||||||
|
}
|
||||||
|
return jsonName, modelDescription, prop
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasNamedJSONTag(field reflect.StructField) bool {
|
||||||
|
parts := strings.Split(field.Tag.Get("json"), ",")
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, s := range parts[1:] {
|
||||||
|
if s == "inline" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(parts[0]) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b modelBuilder) buildStructTypeProperty(field reflect.StructField, jsonName string, model *Model) (nameJson string, prop ModelProperty) {
|
||||||
|
prop.setPropertyMetadata(field)
|
||||||
|
// Check for type override in tag
|
||||||
|
if prop.Type != nil {
|
||||||
|
return jsonName, prop
|
||||||
|
}
|
||||||
|
fieldType := field.Type
|
||||||
|
// check for anonymous
|
||||||
|
if len(fieldType.Name()) == 0 {
|
||||||
|
// anonymous
|
||||||
|
anonType := model.Id + "." + jsonName
|
||||||
|
b.addModel(fieldType, anonType)
|
||||||
|
prop.Ref = &anonType
|
||||||
|
return jsonName, prop
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Name == fieldType.Name() && field.Anonymous && !hasNamedJSONTag(field) {
|
||||||
|
// embedded struct
|
||||||
|
sub := modelBuilder{new(ModelList), b.Config}
|
||||||
|
sub.addModel(fieldType, "")
|
||||||
|
subKey := sub.keyFrom(fieldType)
|
||||||
|
// merge properties from sub
|
||||||
|
subModel, _ := sub.Models.At(subKey)
|
||||||
|
subModel.Properties.Do(func(k string, v ModelProperty) {
|
||||||
|
model.Properties.Put(k, v)
|
||||||
|
// if subModel says this property is required then include it
|
||||||
|
required := false
|
||||||
|
for _, each := range subModel.Required {
|
||||||
|
if k == each {
|
||||||
|
required = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if required {
|
||||||
|
model.Required = append(model.Required, k)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// add all new referenced models
|
||||||
|
sub.Models.Do(func(key string, sub Model) {
|
||||||
|
if key != subKey {
|
||||||
|
if _, ok := b.Models.At(key); !ok {
|
||||||
|
b.Models.Put(key, sub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// empty name signals skip property
|
||||||
|
return "", prop
|
||||||
|
}
|
||||||
|
// simple struct
|
||||||
|
b.addModel(fieldType, "")
|
||||||
|
var pType = b.keyFrom(fieldType)
|
||||||
|
prop.Ref = &pType
|
||||||
|
return jsonName, prop
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b modelBuilder) buildArrayTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop ModelProperty) {
|
||||||
|
// check for type override in tags
|
||||||
|
prop.setPropertyMetadata(field)
|
||||||
|
if prop.Type != nil {
|
||||||
|
return jsonName, prop
|
||||||
|
}
|
||||||
|
fieldType := field.Type
|
||||||
|
if fieldType.Elem().Kind() == reflect.Uint8 {
|
||||||
|
stringt := "string"
|
||||||
|
prop.Type = &stringt
|
||||||
|
return jsonName, prop
|
||||||
|
}
|
||||||
|
var pType = "array"
|
||||||
|
prop.Type = &pType
|
||||||
|
isPrimitive := b.isPrimitiveType(fieldType.Elem().Name())
|
||||||
|
elemTypeName := b.getElementTypeName(modelName, jsonName, fieldType.Elem())
|
||||||
|
prop.Items = new(Item)
|
||||||
|
if isPrimitive {
|
||||||
|
mapped := b.jsonSchemaType(elemTypeName)
|
||||||
|
prop.Items.Type = &mapped
|
||||||
|
} else {
|
||||||
|
prop.Items.Ref = &elemTypeName
|
||||||
|
}
|
||||||
|
// add|overwrite model for element type
|
||||||
|
if fieldType.Elem().Kind() == reflect.Ptr {
|
||||||
|
fieldType = fieldType.Elem()
|
||||||
|
}
|
||||||
|
if !isPrimitive {
|
||||||
|
b.addModel(fieldType.Elem(), elemTypeName)
|
||||||
|
}
|
||||||
|
return jsonName, prop
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b modelBuilder) buildPointerTypeProperty(field reflect.StructField, jsonName, modelName string) (nameJson string, prop ModelProperty) {
|
||||||
|
prop.setPropertyMetadata(field)
|
||||||
|
// Check for type override in tags
|
||||||
|
if prop.Type != nil {
|
||||||
|
return jsonName, prop
|
||||||
|
}
|
||||||
|
fieldType := field.Type
|
||||||
|
|
||||||
|
// override type of pointer to list-likes
|
||||||
|
if fieldType.Elem().Kind() == reflect.Slice || fieldType.Elem().Kind() == reflect.Array {
|
||||||
|
var pType = "array"
|
||||||
|
prop.Type = &pType
|
||||||
|
isPrimitive := b.isPrimitiveType(fieldType.Elem().Elem().Name())
|
||||||
|
elemName := b.getElementTypeName(modelName, jsonName, fieldType.Elem().Elem())
|
||||||
|
if isPrimitive {
|
||||||
|
primName := b.jsonSchemaType(elemName)
|
||||||
|
prop.Items = &Item{Ref: &primName}
|
||||||
|
} else {
|
||||||
|
prop.Items = &Item{Ref: &elemName}
|
||||||
|
}
|
||||||
|
if !isPrimitive {
|
||||||
|
// add|overwrite model for element type
|
||||||
|
b.addModel(fieldType.Elem().Elem(), elemName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// non-array, pointer type
|
||||||
|
fieldTypeName := b.keyFrom(fieldType.Elem())
|
||||||
|
var pType = b.jsonSchemaType(fieldTypeName) // no star, include pkg path
|
||||||
|
if b.isPrimitiveType(fieldTypeName) {
|
||||||
|
prop.Type = &pType
|
||||||
|
prop.Format = b.jsonSchemaFormat(fieldTypeName)
|
||||||
|
return jsonName, prop
|
||||||
|
}
|
||||||
|
prop.Ref = &pType
|
||||||
|
elemName := ""
|
||||||
|
if fieldType.Elem().Name() == "" {
|
||||||
|
elemName = modelName + "." + jsonName
|
||||||
|
prop.Ref = &elemName
|
||||||
|
}
|
||||||
|
b.addModel(fieldType.Elem(), elemName)
|
||||||
|
}
|
||||||
|
return jsonName, prop
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b modelBuilder) getElementTypeName(modelName, jsonName string, t reflect.Type) string {
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
if t.Name() == "" {
|
||||||
|
return modelName + "." + jsonName
|
||||||
|
}
|
||||||
|
return b.keyFrom(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b modelBuilder) keyFrom(st reflect.Type) string {
|
||||||
|
key := st.String()
|
||||||
|
if b.Config != nil && b.Config.ModelTypeNameHandler != nil {
|
||||||
|
if name, ok := b.Config.ModelTypeNameHandler(st); ok {
|
||||||
|
key = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(st.Name()) == 0 { // unnamed type
|
||||||
|
// Swagger UI has special meaning for [
|
||||||
|
key = strings.Replace(key, "[]", "||", -1)
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
// see also https://golang.org/ref/spec#Numeric_types
|
||||||
|
func (b modelBuilder) isPrimitiveType(modelName string) bool {
|
||||||
|
if len(modelName) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.Contains("uint uint8 uint16 uint32 uint64 int int8 int16 int32 int64 float32 float64 bool string byte rune time.Time", modelName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// jsonNameOfField returns the name of the field as it should appear in JSON format
|
||||||
|
// An empty string indicates that this field is not part of the JSON representation
|
||||||
|
func (b modelBuilder) jsonNameOfField(field reflect.StructField) string {
|
||||||
|
if jsonTag := field.Tag.Get("json"); jsonTag != "" {
|
||||||
|
s := strings.Split(jsonTag, ",")
|
||||||
|
if s[0] == "-" {
|
||||||
|
// empty name signals skip property
|
||||||
|
return ""
|
||||||
|
} else if s[0] != "" {
|
||||||
|
return s[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return field.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// see also http://json-schema.org/latest/json-schema-core.html#anchor8
|
||||||
|
func (b modelBuilder) jsonSchemaType(modelName string) string {
|
||||||
|
schemaMap := map[string]string{
|
||||||
|
"uint": "integer",
|
||||||
|
"uint8": "integer",
|
||||||
|
"uint16": "integer",
|
||||||
|
"uint32": "integer",
|
||||||
|
"uint64": "integer",
|
||||||
|
|
||||||
|
"int": "integer",
|
||||||
|
"int8": "integer",
|
||||||
|
"int16": "integer",
|
||||||
|
"int32": "integer",
|
||||||
|
"int64": "integer",
|
||||||
|
|
||||||
|
"byte": "integer",
|
||||||
|
"float64": "number",
|
||||||
|
"float32": "number",
|
||||||
|
"bool": "boolean",
|
||||||
|
"time.Time": "string",
|
||||||
|
}
|
||||||
|
mapped, ok := schemaMap[modelName]
|
||||||
|
if !ok {
|
||||||
|
return modelName // use as is (custom or struct)
|
||||||
|
}
|
||||||
|
return mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b modelBuilder) jsonSchemaFormat(modelName string) string {
|
||||||
|
if b.Config != nil && b.Config.SchemaFormatHandler != nil {
|
||||||
|
if mapped := b.Config.SchemaFormatHandler(modelName); mapped != "" {
|
||||||
|
return mapped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
schemaMap := map[string]string{
|
||||||
|
"int": "int32",
|
||||||
|
"int32": "int32",
|
||||||
|
"int64": "int64",
|
||||||
|
"byte": "byte",
|
||||||
|
"uint": "integer",
|
||||||
|
"uint8": "byte",
|
||||||
|
"float64": "double",
|
||||||
|
"float32": "float",
|
||||||
|
"time.Time": "date-time",
|
||||||
|
"*time.Time": "date-time",
|
||||||
|
}
|
||||||
|
mapped, ok := schemaMap[modelName]
|
||||||
|
if !ok {
|
||||||
|
return "" // no format
|
||||||
|
}
|
||||||
|
return mapped
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package swagger
|
||||||
|
|
||||||
|
// Copyright 2015 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NamedModel associates a name with a Model (not using its Id)
|
||||||
|
type NamedModel struct {
|
||||||
|
Name string
|
||||||
|
Model Model
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelList encapsulates a list of NamedModel (association)
|
||||||
|
type ModelList struct {
|
||||||
|
List []NamedModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put adds or replaces a Model by its name
|
||||||
|
func (l *ModelList) Put(name string, model Model) {
|
||||||
|
for i, each := range l.List {
|
||||||
|
if each.Name == name {
|
||||||
|
// replace
|
||||||
|
l.List[i] = NamedModel{name, model}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// add
|
||||||
|
l.List = append(l.List, NamedModel{name, model})
|
||||||
|
}
|
||||||
|
|
||||||
|
// At returns a Model by its name, ok is false if absent
|
||||||
|
func (l *ModelList) At(name string) (m Model, ok bool) {
|
||||||
|
for _, each := range l.List {
|
||||||
|
if each.Name == name {
|
||||||
|
return each.Model, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do enumerates all the models, each with its assigned name
|
||||||
|
func (l *ModelList) Do(block func(name string, value Model)) {
|
||||||
|
for _, each := range l.List {
|
||||||
|
block(each.Name, each.Model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON writes the ModelList as if it was a map[string]Model
|
||||||
|
func (l ModelList) MarshalJSON() ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
encoder := json.NewEncoder(&buf)
|
||||||
|
buf.WriteString("{\n")
|
||||||
|
for i, each := range l.List {
|
||||||
|
buf.WriteString("\"")
|
||||||
|
buf.WriteString(each.Name)
|
||||||
|
buf.WriteString("\": ")
|
||||||
|
encoder.Encode(each.Model)
|
||||||
|
if i < len(l.List)-1 {
|
||||||
|
buf.WriteString(",\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.WriteString("}")
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON reads back a ModelList. This is an expensive operation.
|
||||||
|
func (l *ModelList) UnmarshalJSON(data []byte) error {
|
||||||
|
raw := map[string]interface{}{}
|
||||||
|
json.NewDecoder(bytes.NewReader(data)).Decode(&raw)
|
||||||
|
for k, v := range raw {
|
||||||
|
// produces JSON bytes for each value
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var m Model
|
||||||
|
json.NewDecoder(bytes.NewReader(data)).Decode(&m)
|
||||||
|
l.Put(k, m)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
81
vendor/github.com/emicklei/go-restful-swagger12/model_property_ext.go
generated
vendored
Normal file
81
vendor/github.com/emicklei/go-restful-swagger12/model_property_ext.go
generated
vendored
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package swagger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (prop *ModelProperty) setDescription(field reflect.StructField) {
|
||||||
|
if tag := field.Tag.Get("description"); tag != "" {
|
||||||
|
prop.Description = tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (prop *ModelProperty) setDefaultValue(field reflect.StructField) {
|
||||||
|
if tag := field.Tag.Get("default"); tag != "" {
|
||||||
|
prop.DefaultValue = Special(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (prop *ModelProperty) setEnumValues(field reflect.StructField) {
|
||||||
|
// We use | to separate the enum values. This value is chosen
|
||||||
|
// since its unlikely to be useful in actual enumeration values.
|
||||||
|
if tag := field.Tag.Get("enum"); tag != "" {
|
||||||
|
prop.Enum = strings.Split(tag, "|")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (prop *ModelProperty) setMaximum(field reflect.StructField) {
|
||||||
|
if tag := field.Tag.Get("maximum"); tag != "" {
|
||||||
|
prop.Maximum = tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (prop *ModelProperty) setType(field reflect.StructField) {
|
||||||
|
if tag := field.Tag.Get("type"); tag != "" {
|
||||||
|
// Check if the first two characters of the type tag are
|
||||||
|
// intended to emulate slice/array behaviour.
|
||||||
|
//
|
||||||
|
// If type is intended to be a slice/array then add the
|
||||||
|
// overriden type to the array item instead of the main property
|
||||||
|
if len(tag) > 2 && tag[0:2] == "[]" {
|
||||||
|
pType := "array"
|
||||||
|
prop.Type = &pType
|
||||||
|
prop.Items = new(Item)
|
||||||
|
|
||||||
|
iType := tag[2:]
|
||||||
|
prop.Items.Type = &iType
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prop.Type = &tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (prop *ModelProperty) setMinimum(field reflect.StructField) {
|
||||||
|
if tag := field.Tag.Get("minimum"); tag != "" {
|
||||||
|
prop.Minimum = tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (prop *ModelProperty) setUniqueItems(field reflect.StructField) {
|
||||||
|
tag := field.Tag.Get("unique")
|
||||||
|
switch tag {
|
||||||
|
case "true":
|
||||||
|
v := true
|
||||||
|
prop.UniqueItems = &v
|
||||||
|
case "false":
|
||||||
|
v := false
|
||||||
|
prop.UniqueItems = &v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (prop *ModelProperty) setPropertyMetadata(field reflect.StructField) {
|
||||||
|
prop.setDescription(field)
|
||||||
|
prop.setEnumValues(field)
|
||||||
|
prop.setMinimum(field)
|
||||||
|
prop.setMaximum(field)
|
||||||
|
prop.setUniqueItems(field)
|
||||||
|
prop.setDefaultValue(field)
|
||||||
|
prop.setType(field)
|
||||||
|
}
|
87
vendor/github.com/emicklei/go-restful-swagger12/model_property_list.go
generated
vendored
Normal file
87
vendor/github.com/emicklei/go-restful-swagger12/model_property_list.go
generated
vendored
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
package swagger
|
||||||
|
|
||||||
|
// Copyright 2015 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NamedModelProperty associates a name to a ModelProperty
|
||||||
|
type NamedModelProperty struct {
|
||||||
|
Name string
|
||||||
|
Property ModelProperty
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelPropertyList encapsulates a list of NamedModelProperty (association)
|
||||||
|
type ModelPropertyList struct {
|
||||||
|
List []NamedModelProperty
|
||||||
|
}
|
||||||
|
|
||||||
|
// At returns the ModelPropety by its name unless absent, then ok is false
|
||||||
|
func (l *ModelPropertyList) At(name string) (p ModelProperty, ok bool) {
|
||||||
|
for _, each := range l.List {
|
||||||
|
if each.Name == name {
|
||||||
|
return each.Property, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put adds or replaces a ModelProperty with this name
|
||||||
|
func (l *ModelPropertyList) Put(name string, prop ModelProperty) {
|
||||||
|
// maybe replace existing
|
||||||
|
for i, each := range l.List {
|
||||||
|
if each.Name == name {
|
||||||
|
// replace
|
||||||
|
l.List[i] = NamedModelProperty{Name: name, Property: prop}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// add
|
||||||
|
l.List = append(l.List, NamedModelProperty{Name: name, Property: prop})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do enumerates all the properties, each with its assigned name
|
||||||
|
func (l *ModelPropertyList) Do(block func(name string, value ModelProperty)) {
|
||||||
|
for _, each := range l.List {
|
||||||
|
block(each.Name, each.Property)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON writes the ModelPropertyList as if it was a map[string]ModelProperty
|
||||||
|
func (l ModelPropertyList) MarshalJSON() ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
encoder := json.NewEncoder(&buf)
|
||||||
|
buf.WriteString("{\n")
|
||||||
|
for i, each := range l.List {
|
||||||
|
buf.WriteString("\"")
|
||||||
|
buf.WriteString(each.Name)
|
||||||
|
buf.WriteString("\": ")
|
||||||
|
encoder.Encode(each.Property)
|
||||||
|
if i < len(l.List)-1 {
|
||||||
|
buf.WriteString(",\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.WriteString("}")
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON reads back a ModelPropertyList. This is an expensive operation.
|
||||||
|
func (l *ModelPropertyList) UnmarshalJSON(data []byte) error {
|
||||||
|
raw := map[string]interface{}{}
|
||||||
|
json.NewDecoder(bytes.NewReader(data)).Decode(&raw)
|
||||||
|
for k, v := range raw {
|
||||||
|
// produces JSON bytes for each value
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var m ModelProperty
|
||||||
|
json.NewDecoder(bytes.NewReader(data)).Decode(&m)
|
||||||
|
l.Put(k, m)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
36
vendor/github.com/emicklei/go-restful-swagger12/ordered_route_map.go
generated
vendored
Normal file
36
vendor/github.com/emicklei/go-restful-swagger12/ordered_route_map.go
generated
vendored
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package swagger
|
||||||
|
|
||||||
|
// Copyright 2015 Ernest Micklei. All rights reserved.
|
||||||
|
// Use of this source code is governed by a license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import "github.com/emicklei/go-restful"
|
||||||
|
|
||||||
|
type orderedRouteMap struct {
|
||||||
|
elements map[string][]restful.Route
|
||||||
|
keys []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOrderedRouteMap() *orderedRouteMap {
|
||||||
|
return &orderedRouteMap{
|
||||||
|
elements: map[string][]restful.Route{},
|
||||||
|
keys: []string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *orderedRouteMap) Add(key string, route restful.Route) {
|
||||||
|
routes, ok := o.elements[key]
|
||||||
|
if ok {
|
||||||
|
routes = append(routes, route)
|
||||||
|
o.elements[key] = routes
|
||||||
|
return
|
||||||
|
}
|
||||||
|
o.elements[key] = []restful.Route{route}
|
||||||
|
o.keys = append(o.keys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *orderedRouteMap) Do(block func(key string, routes []restful.Route)) {
|
||||||
|
for _, k := range o.keys {
|
||||||
|
block(k, o.elements[k])
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue