mirror of
https://github.com/kubesphere/kubekey.git
synced 2025-12-25 17:12:50 +00:00
feat: add web api (#2591)
Signed-off-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
parent
8c84ea7a33
commit
9c87926929
|
|
@ -73,7 +73,7 @@ linters:
|
|||
# - nlreturn
|
||||
- noctx
|
||||
- nolintlint
|
||||
- nonamedreturns
|
||||
# - nonamedreturns
|
||||
- nosprintfhostport
|
||||
# - paralleltest
|
||||
- perfsprint
|
||||
|
|
@ -154,6 +154,10 @@ linters-settings:
|
|||
- k8s.io
|
||||
- oras.land/oras-go
|
||||
- sigs.k8s.io
|
||||
- github.com/emicklei/go-restful/v3
|
||||
- github.com/google/go-cmp/cmp
|
||||
- github.com/emicklei/go-restful-openapi/v2
|
||||
- github.com/go-openapi/spec
|
||||
forbidigo:
|
||||
# Forbid the following identifiers (list of regexp).
|
||||
# Default: ["^(fmt\\.Print(|f|ln)|print|println)$"]
|
||||
|
|
@ -186,6 +190,8 @@ linters-settings:
|
|||
# Ignore comments when counting lines.
|
||||
# Default false
|
||||
ignore-comments: true
|
||||
gocognit:
|
||||
min-complexity: 50
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- experimental
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
- name: Check Connect
|
||||
hosts:
|
||||
- all
|
||||
gather_facts: true
|
||||
tasks:
|
||||
- name: get_arch
|
||||
debug:
|
||||
msg: "{{ .os.architecture }}"
|
||||
|
|
@ -140,7 +140,6 @@ func (o *CommonOptions) Run(ctx context.Context, playbook *kkcorev1.Playbook) er
|
|||
}
|
||||
|
||||
return manager.NewCommandManager(manager.CommandManagerOptions{
|
||||
Workdir: o.Workdir,
|
||||
Playbook: playbook,
|
||||
Config: o.Config,
|
||||
Inventory: o.Inventory,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
cliflag "k8s.io/component-base/cli/flag"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// defaultPort defines the default port number for the web server
|
||||
const (
|
||||
defaultPort = 80
|
||||
)
|
||||
|
||||
// KubeKeyWebOptions contains configuration options for the KubeKey web server
|
||||
type KubeKeyWebOptions struct {
|
||||
Port int // Port specifies the port number for the web server
|
||||
Workdir string // Workdir specifies the base directory for KubeKey
|
||||
|
||||
JSONSchema string
|
||||
}
|
||||
|
||||
// NewKubeKeyWebOptions creates and returns a new KubeKeyWebOptions instance with default values
|
||||
func NewKubeKeyWebOptions() *KubeKeyWebOptions {
|
||||
o := &KubeKeyWebOptions{
|
||||
Port: defaultPort,
|
||||
}
|
||||
// Set the working directory to the current directory joined with "kubekey".
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "get current dir error")
|
||||
o.Workdir = "/root/kubekey"
|
||||
} else {
|
||||
o.Workdir = filepath.Join(wd, "kubekey")
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
// Flags returns a NamedFlagSets object containing command-line flags for configuring the web server
|
||||
func (o *KubeKeyWebOptions) Flags() cliflag.NamedFlagSets {
|
||||
fss := cliflag.NamedFlagSets{}
|
||||
wfs := fss.FlagSet("web flags")
|
||||
wfs.IntVar(&o.Port, "port", o.Port, fmt.Sprintf("the server port of kubekey web default is: %d", o.Port))
|
||||
wfs.StringVar(&o.Workdir, "workdir", o.Workdir, "the base Dir for kubekey. Default current dir. ")
|
||||
wfs.StringVar(&o.JSONSchema, "json-schema", o.JSONSchema, "the json schema to render web ui.")
|
||||
|
||||
return fss
|
||||
}
|
||||
|
|
@ -17,20 +17,10 @@ limitations under the License.
|
|||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
kkcorev1 "github.com/kubesphere/kubekey/api/core/v1"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/rest"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
|
||||
|
||||
"github.com/kubesphere/kubekey/v4/cmd/kk/app/options"
|
||||
_const "github.com/kubesphere/kubekey/v4/pkg/const"
|
||||
"github.com/kubesphere/kubekey/v4/pkg/manager"
|
||||
"github.com/kubesphere/kubekey/v4/pkg/proxy"
|
||||
)
|
||||
|
||||
var internalCommand = make([]*cobra.Command, 0)
|
||||
|
|
@ -62,50 +52,9 @@ func NewRootCommand() *cobra.Command {
|
|||
cmd.AddCommand(newRunCommand())
|
||||
cmd.AddCommand(newPlaybookCommand())
|
||||
cmd.AddCommand(newVersionCommand())
|
||||
cmd.AddCommand(newWebCommand())
|
||||
// internal command
|
||||
cmd.AddCommand(internalCommand...)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// CommandRunE executes the main command logic for the application.
|
||||
// It sets up the necessary configurations, creates the inventory and playbook
|
||||
// resources, and then runs the command manager.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: The context for controlling the execution flow.
|
||||
// - workdir: The working directory path.
|
||||
// - playbook: The playbook resource to be created and managed.
|
||||
// - config: The configuration resource.
|
||||
// - inventory: The inventory resource to be created.
|
||||
//
|
||||
// Returns:
|
||||
// - error: An error if any step in the process fails, otherwise nil.
|
||||
func CommandRunE(ctx context.Context, workdir string, playbook *kkcorev1.Playbook, config *kkcorev1.Config, inventory *kkcorev1.Inventory) error {
|
||||
restconfig := &rest.Config{}
|
||||
if err := proxy.RestConfig(filepath.Join(workdir, _const.RuntimeDir), restconfig); err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := ctrlclient.New(restconfig, ctrlclient.Options{
|
||||
Scheme: _const.Scheme,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get runtime-client")
|
||||
}
|
||||
// create inventory
|
||||
if err := client.Create(ctx, inventory); err != nil {
|
||||
return errors.Wrap(err, "failed to create inventory")
|
||||
}
|
||||
// create playbook
|
||||
if err := client.Create(ctx, playbook); err != nil {
|
||||
return errors.Wrap(err, "failed to create playbook")
|
||||
}
|
||||
|
||||
return manager.NewCommandManager(manager.CommandManagerOptions{
|
||||
Workdir: workdir,
|
||||
Playbook: playbook,
|
||||
Config: config,
|
||||
Inventory: inventory,
|
||||
Client: client,
|
||||
}).Run(ctx)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/rest"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/kubesphere/kubekey/v4/cmd/kk/app/options"
|
||||
_const "github.com/kubesphere/kubekey/v4/pkg/const"
|
||||
"github.com/kubesphere/kubekey/v4/pkg/manager"
|
||||
"github.com/kubesphere/kubekey/v4/pkg/proxy"
|
||||
)
|
||||
|
||||
func newWebCommand() *cobra.Command {
|
||||
o := options.NewKubeKeyWebOptions()
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "web",
|
||||
Short: "start a http server with web UI.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
restconfig := &rest.Config{}
|
||||
if err := proxy.RestConfig(filepath.Join(o.Workdir, _const.RuntimeDir), restconfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := ctrlclient.New(restconfig, ctrlclient.Options{
|
||||
Scheme: _const.Scheme,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return manager.NewWebManager(manager.WebManagerOptions{
|
||||
Workdir: o.Workdir,
|
||||
Port: o.Port,
|
||||
Client: client,
|
||||
Config: restconfig,
|
||||
}).Run(cmd.Context())
|
||||
},
|
||||
}
|
||||
for _, f := range o.Flags().FlagSets {
|
||||
cmd.Flags().AddFlagSet(f)
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package config
|
||||
|
||||
import "embed"
|
||||
|
||||
// Swagger embeds the swagger-ui directory containing the OpenAPI/Swagger documentation UI
|
||||
// This allows serving the Swagger UI directly from the binary without needing external files
|
||||
//
|
||||
//go:embed swagger-ui
|
||||
var Swagger embed.FS
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 665 B |
Binary file not shown.
|
After Width: | Height: | Size: 628 B |
|
|
@ -0,0 +1,16 @@
|
|||
html {
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<!-- HTML for static distribution bundle build -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Swagger UI</title>
|
||||
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
|
||||
<link rel="stylesheet" type="text/css" href="index.css" />
|
||||
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
|
||||
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
|
||||
<script src="./swagger-initializer.js" charset="UTF-8"> </script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<title>Swagger UI: OAuth2 Redirect</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
'use strict';
|
||||
function run () {
|
||||
var oauth2 = window.opener.swaggerUIRedirectOauth2;
|
||||
var sentState = oauth2.state;
|
||||
var redirectUrl = oauth2.redirectUrl;
|
||||
var isValid, qp, arr;
|
||||
|
||||
if (/code|token|error/.test(window.location.hash)) {
|
||||
qp = window.location.hash.substring(1).replace('?', '&');
|
||||
} else {
|
||||
qp = location.search.substring(1);
|
||||
}
|
||||
|
||||
arr = qp.split("&");
|
||||
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
|
||||
qp = qp ? JSON.parse('{' + arr.join() + '}',
|
||||
function (key, value) {
|
||||
return key === "" ? value : decodeURIComponent(value);
|
||||
}
|
||||
) : {};
|
||||
|
||||
isValid = qp.state === sentState;
|
||||
|
||||
if ((
|
||||
oauth2.auth.schema.get("flow") === "accessCode" ||
|
||||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
|
||||
oauth2.auth.schema.get("flow") === "authorization_code"
|
||||
) && !oauth2.auth.code) {
|
||||
if (!isValid) {
|
||||
oauth2.errCb({
|
||||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "warning",
|
||||
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
|
||||
});
|
||||
}
|
||||
|
||||
if (qp.code) {
|
||||
delete oauth2.state;
|
||||
oauth2.auth.code = qp.code;
|
||||
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
|
||||
} else {
|
||||
let oauthErrorMsg;
|
||||
if (qp.error) {
|
||||
oauthErrorMsg = "["+qp.error+"]: " +
|
||||
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
|
||||
(qp.error_uri ? "More info: "+qp.error_uri : "");
|
||||
}
|
||||
|
||||
oauth2.errCb({
|
||||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "error",
|
||||
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
|
||||
});
|
||||
}
|
||||
} else {
|
||||
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
|
||||
}
|
||||
window.close();
|
||||
}
|
||||
|
||||
if (document.readyState !== 'loading') {
|
||||
run();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
run();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
window.onload = function() {
|
||||
//<editor-fold desc="Changeable Configuration Block">
|
||||
|
||||
// the following lines will be replaced by docker/configurator, when it runs in a docker-container
|
||||
window.ui = SwaggerUIBundle({
|
||||
url: "/apidocs.json",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "StandaloneLayout"
|
||||
});
|
||||
|
||||
//</editor-fold>
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
16
go.mod
16
go.mod
|
|
@ -6,8 +6,12 @@ require (
|
|||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/cockroachdb/errors v1.11.3
|
||||
github.com/containerd/containerd v1.7.27
|
||||
github.com/emicklei/go-restful-openapi/v2 v2.11.0
|
||||
github.com/emicklei/go-restful/v3 v3.12.2
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/go-git/go-git/v5 v5.11.0
|
||||
github.com/go-openapi/spec v0.21.0
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/gops v0.3.28
|
||||
github.com/kubesphere/kubekey/api v0.0.0-00010101000000-000000000000
|
||||
github.com/opencontainers/image-spec v1.1.0
|
||||
|
|
@ -15,7 +19,7 @@ require (
|
|||
github.com/schollz/progressbar/v3 v3.14.5
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/crypto v0.31.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/api v0.31.3
|
||||
|
|
@ -54,7 +58,6 @@ require (
|
|||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
|
|
@ -64,16 +67,15 @@ require (
|
|||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/gobuffalo/flect v1.0.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/cel-go v0.20.1 // indirect
|
||||
github.com/google/gnostic-models v0.6.9 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
||||
|
|
@ -89,7 +91,7 @@ require (
|
|||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
|
|
|
|||
46
go.sum
46
go.sum
|
|
@ -69,8 +69,11 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
|||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
|
||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||
github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/emicklei/go-restful-openapi/v2 v2.11.0 h1:Ur+yGxoOH/7KRmcj/UoMFqC3VeNc9VOe+/XidumxTvk=
|
||||
github.com/emicklei/go-restful-openapi/v2 v2.11.0/go.mod h1:4CTuOXHFg3jkvCpnXN+Wkw5prVUnP8hIACssJTYorWo=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI=
|
||||
|
|
@ -106,14 +109,20 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
|||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
|
||||
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4=
|
||||
|
|
@ -179,15 +188,17 @@ github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQs
|
|||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||
|
|
@ -202,6 +213,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
|||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.36.0 h1:Pb12RlruUtj4XUuPUqeEWc6j5DkVVVA49Uf6YLfC95Y=
|
||||
|
|
@ -264,12 +276,13 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
|||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
|
|
@ -423,7 +436,9 @@ google.golang.org/grpc v1.65.1/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjr
|
|||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
||||
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
|
|
@ -440,6 +455,7 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -31,6 +31,11 @@ var (
|
|||
CapkkVolumeProject = Environment{env: "CAPKK_VOLUME_PROJECT"}
|
||||
// CapkkVolumeWorkdir specifies the working directory for capkk playbook
|
||||
CapkkVolumeWorkdir = Environment{env: "CAPKK_VOLUME_WORKDIR"}
|
||||
|
||||
// TaskNameGatherFacts the task name for gather_facts in playbook
|
||||
TaskNameGatherFacts = Environment{env: "TASK_GATHER_FACTS", def: "gather_facts"}
|
||||
// TaskNameGetArch the task name for get_arch in playbook, used to get host architecture
|
||||
TaskNameGetArch = Environment{env: "", def: "get_arch"}
|
||||
)
|
||||
|
||||
// Getenv retrieves the value of the environment variable. If the environment variable is not set,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
package _const
|
||||
|
||||
import "k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
const (
|
||||
// APIPath defines the base path for API endpoints in the KubeKey API server.
|
||||
// This path is used as the prefix for all API routes, including those for
|
||||
// managing inventories, playbooks, and other KubeKey resources.
|
||||
APIPath = "/kapis/"
|
||||
|
||||
// KubeKeyTag is the tag used for KubeKey related resources
|
||||
// This tag is used to identify and categorize KubeKey-specific resources
|
||||
// in the system, making it easier to filter and manage them
|
||||
KubeKeyTag = "kubekey"
|
||||
|
||||
// OpenAPITag is the tag used for OpenAPI documentation
|
||||
// This tag helps organize and identify OpenAPI/Swagger documentation
|
||||
// related to the KubeKey API endpoints
|
||||
OpenAPITag = "api"
|
||||
|
||||
// StatusOK represents a successful operation status
|
||||
// Used to indicate that an API operation completed successfully
|
||||
// without any errors or issues
|
||||
StatusOK = "ok"
|
||||
)
|
||||
|
||||
// SUCCESS is a predefined successful result
|
||||
// This is a convenience variable that provides a standard success response
|
||||
// for API operations that don't need to return specific data
|
||||
var SUCCESS = Result{Message: "success"}
|
||||
|
||||
// Result represents a generic API response with a message
|
||||
// This type is used for simple API responses that only need to convey
|
||||
// a status message, such as success or error notifications
|
||||
type Result struct {
|
||||
Message string `description:"error message" json:"message"`
|
||||
}
|
||||
|
||||
// ListResult represents a paginated list response containing items and total count
|
||||
// This type is used for API responses that return a list of items with pagination
|
||||
// support, allowing clients to handle large datasets efficiently
|
||||
type ListResult struct {
|
||||
Items []runtime.Object `json:"items"` // The list of items in the current page
|
||||
TotalItems int `json:"totalItems"` // The total number of items across all pages
|
||||
}
|
||||
|
|
@ -18,7 +18,11 @@ package executor
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
kkcorev1 "github.com/kubesphere/kubekey/api/core/v1"
|
||||
|
|
@ -28,6 +32,7 @@ import (
|
|||
"k8s.io/klog/v2"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
_const "github.com/kubesphere/kubekey/v4/pkg/const"
|
||||
"github.com/kubesphere/kubekey/v4/pkg/converter"
|
||||
"github.com/kubesphere/kubekey/v4/pkg/project"
|
||||
"github.com/kubesphere/kubekey/v4/pkg/variable"
|
||||
|
|
@ -60,7 +65,21 @@ type playbookExecutor struct {
|
|||
}
|
||||
|
||||
// Exec playbook. covert playbook to block and executor it.
|
||||
func (e playbookExecutor) Exec(ctx context.Context) error {
|
||||
func (e playbookExecutor) Exec(ctx context.Context) (retErr error) {
|
||||
defer e.syncStatus(ctx, retErr)
|
||||
fmt.Fprint(e.logOutput, `
|
||||
|
||||
_ __ _ _ __
|
||||
| | / / | | | | / /
|
||||
| |/ / _ _| |__ ___| |/ / ___ _ _
|
||||
| \| | | | '_ \ / _ \ \ / _ \ | | |
|
||||
| |\ \ |_| | |_) | __/ |\ \ __/ |_| |
|
||||
\_| \_/\__,_|_.__/ \___\_| \_/\___|\__, |
|
||||
__/ |
|
||||
|___/
|
||||
|
||||
`)
|
||||
fmt.Fprintf(e.logOutput, "%s [Playbook %s] start\n", time.Now().Format(time.TimeOnly+" MST"), ctrlclient.ObjectKeyFromObject(e.playbook))
|
||||
klog.V(5).InfoS("deal project", "playbook", ctrlclient.ObjectKeyFromObject(e.playbook))
|
||||
pj, err := project.New(ctx, *e.playbook, true)
|
||||
if err != nil {
|
||||
|
|
@ -103,6 +122,36 @@ func (e playbookExecutor) Exec(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (e playbookExecutor) syncStatus(ctx context.Context, err error) {
|
||||
cp := e.playbook.DeepCopy()
|
||||
if err != nil {
|
||||
e.playbook.Status.Phase = kkcorev1.PlaybookPhaseFailed
|
||||
e.playbook.Status.FailureReason = kkcorev1.PlaybookFailedReasonTaskFailed
|
||||
e.playbook.Status.FailureMessage = err.Error()
|
||||
} else {
|
||||
e.playbook.Status.Phase = kkcorev1.PlaybookPhaseSucceeded
|
||||
}
|
||||
|
||||
fmt.Fprintf(e.logOutput, "%s [Playbook %s] finish. total: %v,success: %v,ignored: %v,failed: %v\n", time.Now().Format(time.TimeOnly+" MST"), ctrlclient.ObjectKeyFromObject(e.playbook),
|
||||
e.playbook.Status.TaskResult.Total, e.playbook.Status.TaskResult.Success, e.playbook.Status.TaskResult.Ignored, e.playbook.Status.TaskResult.Failed)
|
||||
|
||||
// update playbook status
|
||||
if err := e.client.Status().Patch(ctx, e.playbook, ctrlclient.MergeFrom(cp)); err != nil {
|
||||
klog.ErrorS(err, "update playbook error", "playbook", ctrlclient.ObjectKeyFromObject(e.playbook))
|
||||
}
|
||||
|
||||
go func() {
|
||||
if !e.playbook.Spec.Debug && e.playbook.Status.Phase == kkcorev1.PlaybookPhaseSucceeded {
|
||||
<-ctx.Done()
|
||||
fmt.Fprintf(e.logOutput, "%s [Playbook %s] clean runtime directory\n", time.Now().Format(time.TimeOnly+" MST"), ctrlclient.ObjectKeyFromObject(e.playbook))
|
||||
// clean runtime directory
|
||||
if err := os.RemoveAll(filepath.Join(_const.GetWorkdirFromConfig(e.playbook.Spec.Config), _const.RuntimeDir)); err != nil {
|
||||
klog.ErrorS(err, "clean runtime directory error", "playbook", ctrlclient.ObjectKeyFromObject(e.playbook), "runtime_dir", filepath.Join(_const.GetWorkdirFromConfig(e.playbook.Spec.Config), _const.RuntimeDir))
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// execBatchHosts executor block in play order by: "pre_tasks" > "roles" > "tasks" > "post_tasks"
|
||||
func (e playbookExecutor) execBatchHosts(ctx context.Context, play kkprojectv1.Play, batchHosts [][]string) error {
|
||||
// generate and execute task.
|
||||
|
|
|
|||
|
|
@ -18,22 +18,15 @@ package manager
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
kkcorev1 "github.com/kubesphere/kubekey/api/core/v1"
|
||||
"k8s.io/klog/v2"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
_const "github.com/kubesphere/kubekey/v4/pkg/const"
|
||||
"github.com/kubesphere/kubekey/v4/pkg/executor"
|
||||
)
|
||||
|
||||
type commandManager struct {
|
||||
workdir string
|
||||
*kkcorev1.Playbook
|
||||
*kkcorev1.Inventory
|
||||
|
||||
|
|
@ -44,48 +37,5 @@ type commandManager struct {
|
|||
|
||||
// Run command Manager. print log and run playbook executor.
|
||||
func (m *commandManager) Run(ctx context.Context) error {
|
||||
fmt.Fprint(m.logOutput, `
|
||||
|
||||
_ __ _ _ __
|
||||
| | / / | | | | / /
|
||||
| |/ / _ _| |__ ___| |/ / ___ _ _
|
||||
| \| | | | '_ \ / _ \ \ / _ \ | | |
|
||||
| |\ \ |_| | |_) | __/ |\ \ __/ |_| |
|
||||
\_| \_/\__,_|_.__/ \___\_| \_/\___|\__, |
|
||||
__/ |
|
||||
|___/
|
||||
|
||||
`)
|
||||
fmt.Fprintf(m.logOutput, "%s [Playbook %s] start\n", time.Now().Format(time.TimeOnly+" MST"), ctrlclient.ObjectKeyFromObject(m.Playbook))
|
||||
cp := m.Playbook.DeepCopy()
|
||||
defer func() {
|
||||
fmt.Fprintf(m.logOutput, "%s [Playbook %s] finish. total: %v,success: %v,ignored: %v,failed: %v\n", time.Now().Format(time.TimeOnly+" MST"), ctrlclient.ObjectKeyFromObject(m.Playbook),
|
||||
m.Playbook.Status.TaskResult.Total, m.Playbook.Status.TaskResult.Success, m.Playbook.Status.TaskResult.Ignored, m.Playbook.Status.TaskResult.Failed)
|
||||
go func() {
|
||||
if !m.Playbook.Spec.Debug && m.Playbook.Status.Phase == kkcorev1.PlaybookPhaseSucceeded {
|
||||
<-ctx.Done()
|
||||
fmt.Fprintf(m.logOutput, "%s [Playbook %s] clean runtime directory\n", time.Now().Format(time.TimeOnly+" MST"), ctrlclient.ObjectKeyFromObject(m.Playbook))
|
||||
// clean runtime directory
|
||||
if err := os.RemoveAll(filepath.Join(m.workdir, _const.RuntimeDir)); err != nil {
|
||||
klog.ErrorS(err, "clean runtime directory error", "playbook", ctrlclient.ObjectKeyFromObject(m.Playbook), "runtime_dir", filepath.Join(m.workdir, _const.RuntimeDir))
|
||||
}
|
||||
}
|
||||
}()
|
||||
// update playbook status
|
||||
if err := m.Client.Status().Patch(ctx, m.Playbook, ctrlclient.MergeFrom(cp)); err != nil {
|
||||
klog.ErrorS(err, "update playbook error", "playbook", ctrlclient.ObjectKeyFromObject(m.Playbook))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := executor.NewPlaybookExecutor(ctx, m.Client, m.Playbook, m.logOutput).Exec(ctx); err != nil {
|
||||
klog.ErrorS(err, "executor tasks error", "playbook", ctrlclient.ObjectKeyFromObject(m.Playbook))
|
||||
m.Playbook.Status.Phase = kkcorev1.PlaybookPhaseFailed
|
||||
m.Playbook.Status.FailureReason = kkcorev1.PlaybookFailedReasonTaskFailed
|
||||
m.Playbook.Status.FailureMessage = err.Error()
|
||||
|
||||
return err
|
||||
}
|
||||
m.Playbook.Status.Phase = kkcorev1.PlaybookPhaseSucceeded
|
||||
|
||||
return nil
|
||||
return executor.NewPlaybookExecutor(ctx, m.Client, m.Playbook, m.logOutput).Exec(ctx)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,20 +21,20 @@ import (
|
|||
"os"
|
||||
|
||||
kkcorev1 "github.com/kubesphere/kubekey/api/core/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/kubesphere/kubekey/v4/cmd/controller-manager/app/options"
|
||||
)
|
||||
|
||||
// Manager shared dependencies such as Addr and , and provides them to Runnable.
|
||||
// Manager defines the interface for different types of managers that can run operations
|
||||
type Manager interface {
|
||||
// Run the driver
|
||||
// Run executes the manager's main functionality with the given context
|
||||
Run(ctx context.Context) error
|
||||
}
|
||||
|
||||
// CommandManagerOptions for NewCommandManager
|
||||
// CommandManagerOptions contains the configuration options for creating a new command manager
|
||||
type CommandManagerOptions struct {
|
||||
Workdir string
|
||||
*kkcorev1.Playbook
|
||||
*kkcorev1.Config
|
||||
*kkcorev1.Inventory
|
||||
|
|
@ -42,10 +42,9 @@ type CommandManagerOptions struct {
|
|||
ctrlclient.Client
|
||||
}
|
||||
|
||||
// NewCommandManager return a new commandManager
|
||||
// NewCommandManager creates and returns a new command manager instance with the provided options
|
||||
func NewCommandManager(o CommandManagerOptions) Manager {
|
||||
return &commandManager{
|
||||
workdir: o.Workdir,
|
||||
Playbook: o.Playbook,
|
||||
Inventory: o.Inventory,
|
||||
Client: o.Client,
|
||||
|
|
@ -53,9 +52,27 @@ func NewCommandManager(o CommandManagerOptions) Manager {
|
|||
}
|
||||
}
|
||||
|
||||
// NewControllerManager return a new controllerManager
|
||||
// NewControllerManager creates and returns a new controller manager instance with the provided options
|
||||
func NewControllerManager(o *options.ControllerManagerServerOptions) Manager {
|
||||
return &controllerManager{
|
||||
ControllerManagerServerOptions: o,
|
||||
}
|
||||
}
|
||||
|
||||
// WebManagerOptions contains the configuration options for creating a new web manager
|
||||
type WebManagerOptions struct {
|
||||
Workdir string
|
||||
Port int
|
||||
ctrlclient.Client
|
||||
*rest.Config
|
||||
}
|
||||
|
||||
// NewWebManager creates and returns a new web manager instance with the provided options
|
||||
func NewWebManager(o WebManagerOptions) Manager {
|
||||
return &webManager{
|
||||
workdir: o.Workdir,
|
||||
port: o.Port,
|
||||
Client: o.Client,
|
||||
Config: o.Config,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/klog/v2"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/kubesphere/kubekey/v4/pkg/web"
|
||||
)
|
||||
|
||||
// webManager handles the web server functionality for the application
|
||||
type webManager struct {
|
||||
port int
|
||||
workdir string
|
||||
|
||||
ctrlclient.Client
|
||||
*rest.Config
|
||||
}
|
||||
|
||||
// Run starts the web server and handles incoming requests
|
||||
func (m webManager) Run(ctx context.Context) error {
|
||||
container := restful.DefaultContainer
|
||||
container.Filter(logRequestAndResponse)
|
||||
container.RecoverHandler(func(panicReason any, httpWriter http.ResponseWriter) {
|
||||
logStackOnRecover(panicReason, httpWriter)
|
||||
})
|
||||
container.Add(web.NewWebService(ctx, m.workdir, m.Client, m.Config)).
|
||||
// openapi
|
||||
Add(web.NewSwaggerUIService()).
|
||||
Add(web.NewAPIService(container.RegisteredWebServices()))
|
||||
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", m.port),
|
||||
Handler: container,
|
||||
ReadHeaderTimeout: 10 * time.Second, // Prevent Slowloris attack by timing out slow headers
|
||||
}
|
||||
|
||||
shutdownCtx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = server.Shutdown(shutdownCtx)
|
||||
}()
|
||||
|
||||
return server.ListenAndServe()
|
||||
}
|
||||
|
||||
// logStackOnRecover handles panic recovery and logs the stack trace
|
||||
func logStackOnRecover(panicReason any, w http.ResponseWriter) {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(fmt.Sprintf("recover from panic: %v\n", panicReason))
|
||||
for i := 2; ; i++ {
|
||||
_, file, line, ok := runtime.Caller(i)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf(" %s:%d\n", file, line))
|
||||
}
|
||||
klog.Errorln(buf.String())
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte("Internal Server Error"))
|
||||
}
|
||||
|
||||
// logRequestAndResponse logs HTTP request and response details
|
||||
func logRequestAndResponse(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
|
||||
start := time.Now()
|
||||
chain.ProcessFilter(req, resp)
|
||||
|
||||
// Always log error response
|
||||
logWithVerbose := klog.V(4)
|
||||
if resp.StatusCode() > http.StatusBadRequest {
|
||||
logWithVerbose = klog.V(0)
|
||||
}
|
||||
|
||||
logWithVerbose.Infof("%s - \"%s %s %s\" %d %d %dms",
|
||||
remoteIP(req.Request),
|
||||
req.Request.Method,
|
||||
req.Request.URL,
|
||||
req.Request.Proto,
|
||||
resp.StatusCode(),
|
||||
resp.ContentLength(),
|
||||
time.Since(start)/time.Millisecond,
|
||||
)
|
||||
}
|
||||
|
||||
// remoteIP extracts the client IP address from the request, handling various proxy headers
|
||||
func remoteIP(req *http.Request) string {
|
||||
remoteAddr := req.RemoteAddr
|
||||
if ip := req.Header.Get("X-Client-Ip"); ip != "" {
|
||||
remoteAddr = ip
|
||||
} else if ip := req.Header.Get("X-Real-IP"); ip != "" {
|
||||
remoteAddr = ip
|
||||
} else if ip = req.Header.Get("X-Forwarded-For"); ip != "" {
|
||||
remoteAddr = ip
|
||||
} else {
|
||||
remoteAddr, _, _ = net.SplitHostPort(remoteAddr)
|
||||
}
|
||||
|
||||
if remoteAddr == "::1" {
|
||||
remoteAddr = "127.0.0.1"
|
||||
}
|
||||
|
||||
return remoteAddr
|
||||
}
|
||||
|
|
@ -29,15 +29,16 @@ import (
|
|||
"github.com/kubesphere/kubekey/v4/pkg/variable"
|
||||
)
|
||||
|
||||
// message for stdout
|
||||
const (
|
||||
// StdoutSuccess message for common module
|
||||
// StdoutSuccess is the standard message indicating a successful module execution.
|
||||
StdoutSuccess = "success"
|
||||
StdoutSkip = "skip"
|
||||
|
||||
// StdoutTrue for bool module
|
||||
// StdoutFailed is the standard message indicating a failed module execution.
|
||||
StdoutFailed = "failed"
|
||||
// StdoutSkip is the standard message indicating a skipped module execution.
|
||||
StdoutSkip = "skip"
|
||||
// StdoutTrue is the standard message indicating a boolean true result (used in bool/assert modules).
|
||||
StdoutTrue = "True"
|
||||
// StdoutFalse for bool module
|
||||
// StdoutFalse is the standard message indicating a boolean false result (used in bool/assert modules).
|
||||
StdoutFalse = "False"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,17 +32,17 @@ func ModuleSetup(ctx context.Context, options ExecOptions) (string, string) {
|
|||
// get connector
|
||||
conn, err := getConnector(ctx, options.Host, options.Variable)
|
||||
if err != nil {
|
||||
return "", fmt.Sprintf("failed to connector of %q error: %v", options.Host, err)
|
||||
return StdoutFailed, fmt.Sprintf("failed to connector of %q error: %v", options.Host, err)
|
||||
}
|
||||
defer conn.Close(ctx)
|
||||
|
||||
if gf, ok := conn.(connector.GatherFacts); ok {
|
||||
remoteInfo, err := gf.HostInfo(ctx)
|
||||
if err != nil {
|
||||
return "", err.Error()
|
||||
return StdoutFailed, err.Error()
|
||||
}
|
||||
if err := options.Variable.Merge(variable.MergeRemoteVariable(remoteInfo, options.Host)); err != nil {
|
||||
return "", err.Error()
|
||||
return StdoutFailed, err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -169,11 +169,20 @@ func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
|
|||
|
||||
// ToSelectableFields returns a field set that represents the object
|
||||
func ToSelectableFields(task *kkcorev1alpha1.Task) fields.Set {
|
||||
// The purpose of allocation with a given number of elements is to reduce
|
||||
// amount of allocations needed to create the fields.Set. If you add any
|
||||
// field here or the number of object-meta related fields changes, this should
|
||||
// be adjusted.
|
||||
return apigeneric.AddObjectMetaFieldsSet(apigeneric.ObjectMetaFieldsSet(&task.ObjectMeta, true), &task.ObjectMeta, true)
|
||||
objectMetaFieldsSet := apigeneric.ObjectMetaFieldsSet(&task.ObjectMeta, true)
|
||||
taskSpecificFieldsSet := fields.Set{
|
||||
"spec.name": task.Spec.Name,
|
||||
}
|
||||
for _, reference := range task.OwnerReferences {
|
||||
if reference.Kind == playbookKind {
|
||||
taskSpecificFieldsSet["playbook.name"] = reference.Name
|
||||
taskSpecificFieldsSet["playbook.uid"] = string(reference.UID)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return apigeneric.MergeFieldsSets(taskSpecificFieldsSet, objectMetaFieldsSet)
|
||||
}
|
||||
|
||||
// NameTriggerFunc returns value metadata.namespace of given object.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
Copyright 2020 The KubeSphere Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// sanitizer is a string replacer that converts HTML special characters to their escaped equivalents
|
||||
// to prevent XSS attacks in error messages. It replaces &, <, and > with their HTML entities.
|
||||
var sanitizer = strings.NewReplacer(`&`, "&", `<`, "<", `>`, ">")
|
||||
|
||||
// HandleInternalError handles internal server errors (500) by logging the error and sending an appropriate response
|
||||
func HandleInternalError(response *restful.Response, req *restful.Request, err error) {
|
||||
handle(http.StatusInternalServerError, response, req, err)
|
||||
}
|
||||
|
||||
// HandleBadRequest handles bad request errors (400) by logging the error and sending an appropriate response
|
||||
func HandleBadRequest(response *restful.Response, req *restful.Request, err error) {
|
||||
handle(http.StatusBadRequest, response, req, err)
|
||||
}
|
||||
|
||||
// HandleNotFound handles not found errors (404) by logging the error and sending an appropriate response
|
||||
func HandleNotFound(response *restful.Response, req *restful.Request, err error) {
|
||||
handle(http.StatusNotFound, response, req, err)
|
||||
}
|
||||
|
||||
// HandleForbidden handles forbidden errors (403) by logging the error and sending an appropriate response
|
||||
func HandleForbidden(response *restful.Response, req *restful.Request, err error) {
|
||||
handle(http.StatusForbidden, response, req, err)
|
||||
}
|
||||
|
||||
// HandleUnauthorized handles unauthorized errors (401) by logging the error and sending an appropriate response
|
||||
func HandleUnauthorized(response *restful.Response, req *restful.Request, err error) {
|
||||
handle(http.StatusUnauthorized, response, req, err)
|
||||
}
|
||||
|
||||
// HandleTooManyRequests handles rate limiting errors (429) by logging the error and sending an appropriate response
|
||||
func HandleTooManyRequests(response *restful.Response, req *restful.Request, err error) {
|
||||
handle(http.StatusTooManyRequests, response, req, err)
|
||||
}
|
||||
|
||||
// HandleConflict handles conflict errors (409) by logging the error and sending an appropriate response
|
||||
func HandleConflict(response *restful.Response, req *restful.Request, err error) {
|
||||
handle(http.StatusConflict, response, req, err)
|
||||
}
|
||||
|
||||
// HandleError handles various types of errors by determining the appropriate status code
|
||||
// and sending a response with the corresponding HTTP status code
|
||||
func HandleError(response *restful.Response, req *restful.Request, err error) {
|
||||
var statusCode int
|
||||
var apiStatus apierrors.APIStatus
|
||||
var serviceError restful.ServiceError
|
||||
|
||||
switch {
|
||||
case errors.As(err, &apiStatus):
|
||||
statusCode = int(apiStatus.Status().Code)
|
||||
case errors.As(err, &serviceError):
|
||||
statusCode = serviceError.Code
|
||||
default:
|
||||
statusCode = http.StatusInternalServerError
|
||||
}
|
||||
handle(statusCode, response, req, err)
|
||||
}
|
||||
|
||||
// handle is an internal helper function that logs the error and sends an HTTP error response
|
||||
// with the sanitized error message and specified status code
|
||||
func handle(statusCode int, response *restful.Response, _ *restful.Request, err error) {
|
||||
_, fn, line, _ := runtime.Caller(2)
|
||||
klog.Errorf("%s:%d %v", fn, line, err)
|
||||
http.Error(response, sanitizer.Replace(err.Error()), statusCode)
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
Copyright 2019 The KubeSphere Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
// SUCCESS represents a successful operation result with a default success message
|
||||
var SUCCESS = Result{Message: "success"}
|
||||
|
||||
// Result represents a basic API response with a message field
|
||||
type Result struct {
|
||||
Message string `description:"error message" json:"message"`
|
||||
}
|
||||
|
||||
// ListResult represents a paginated list response containing items and total count
|
||||
type ListResult[T any] struct {
|
||||
Items []T `json:"items"`
|
||||
TotalItems int `json:"totalItems"`
|
||||
}
|
||||
|
||||
// InventoryHostTable represents a host entry in an inventory with its configuration details
|
||||
// It includes network information, SSH credentials, and group membership
|
||||
type InventoryHostTable struct {
|
||||
Name string `json:"name"` // Hostname of the inventory host
|
||||
Status string `json:"status"` // Current status of the host
|
||||
InternalIPV4 string `json:"internalIPV4"` // IPv4 address of the host
|
||||
InternalIPV6 string `json:"internalIPV6"` // IPv6 address of the host
|
||||
SSHHost string `json:"sshHost"` // SSH hostname for connection
|
||||
SSHPort string `json:"sshPort"` // SSH port for connection
|
||||
SSHUser string `json:"sshUser"` // SSH username for authentication
|
||||
SSHPassword string `json:"sshPassword"` // SSH password for authentication
|
||||
SSHPrivateKey string `json:"sshPrivateKey"` // SSH private key for authentication
|
||||
Vars map[string]any `json:"vars"` // Additional host variables
|
||||
Groups []string `json:"groups"` // Groups the host belongs to
|
||||
Arch string `json:"arch"` // Architecture of the host
|
||||
}
|
||||
|
|
@ -0,0 +1,459 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
kkcorev1 "github.com/kubesphere/kubekey/api/core/v1"
|
||||
kkcorev1alpha1 "github.com/kubesphere/kubekey/api/core/v1alpha1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/klog/v2"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
_const "github.com/kubesphere/kubekey/v4/pkg/const"
|
||||
"github.com/kubesphere/kubekey/v4/pkg/executor"
|
||||
"github.com/kubesphere/kubekey/v4/pkg/variable"
|
||||
"github.com/kubesphere/kubekey/v4/pkg/web/api"
|
||||
"github.com/kubesphere/kubekey/v4/pkg/web/query"
|
||||
)
|
||||
|
||||
// newHandler creates a new handler instance with the given workdir, client and config
|
||||
// workdir: Base directory for storing work files
|
||||
// client: Kubernetes client for API operations
|
||||
// config: Kubernetes REST client configuration
|
||||
func newHandler(workdir string, client ctrlclient.Client, config *rest.Config) *handler {
|
||||
return &handler{workdir: workdir, client: client, restconfig: config}
|
||||
}
|
||||
|
||||
// handler implements HTTP handlers for managing inventories and playbooks
|
||||
// It provides methods for CRUD operations on inventories and playbooks
|
||||
type handler struct {
|
||||
workdir string // Base directory for storing work files
|
||||
restconfig *rest.Config // Kubernetes REST client configuration
|
||||
client ctrlclient.Client // Kubernetes client for API operations
|
||||
}
|
||||
|
||||
// createInventory creates a new inventory resource
|
||||
// It reads the inventory from the request body and creates it in the Kubernetes cluster
|
||||
func (h handler) createInventory(request *restful.Request, response *restful.Response) {
|
||||
inventory := &kkcorev1.Inventory{}
|
||||
if err := request.ReadEntity(inventory); err != nil {
|
||||
api.HandleBadRequest(response, request, err)
|
||||
return
|
||||
}
|
||||
if err := h.client.Create(request.Request.Context(), inventory); err != nil {
|
||||
api.HandleBadRequest(response, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
_ = response.WriteEntity(inventory)
|
||||
}
|
||||
|
||||
// patchInventory patches an existing inventory resource
|
||||
// It reads the patch data from the request body and applies it to the specified inventory
|
||||
func (h handler) patchInventory(request *restful.Request, response *restful.Response) {
|
||||
namespace := request.PathParameter("namespace")
|
||||
name := request.PathParameter("inventory")
|
||||
data, err := io.ReadAll(request.Request.Body)
|
||||
if err != nil {
|
||||
api.HandleBadRequest(response, request, err)
|
||||
return
|
||||
}
|
||||
patchType := request.HeaderParameter("Content-Type")
|
||||
|
||||
// get old inventory
|
||||
inventory := &kkcorev1.Inventory{}
|
||||
if err := h.client.Get(request.Request.Context(), ctrlclient.ObjectKey{Namespace: namespace, Name: name}, inventory); err != nil {
|
||||
api.HandleBadRequest(response, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.client.Patch(request.Request.Context(), inventory, ctrlclient.RawPatch(types.PatchType(patchType), data)); err != nil {
|
||||
api.HandleBadRequest(response, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
_ = response.WriteEntity(inventory)
|
||||
}
|
||||
|
||||
// listInventories lists all inventory resources with optional filtering and sorting
|
||||
// It supports field selectors and label selectors for filtering the results
|
||||
func (h handler) listInventories(request *restful.Request, response *restful.Response) {
|
||||
queryParam := query.ParseQueryParameter(request)
|
||||
var fieldselector fields.Selector
|
||||
if v, ok := queryParam.Filters[query.ParameterFieldSelector]; ok {
|
||||
fs, err := fields.ParseSelector(string(v))
|
||||
if err != nil {
|
||||
api.HandleError(response, request, err)
|
||||
return
|
||||
}
|
||||
fieldselector = fs
|
||||
}
|
||||
namespace := request.PathParameter("namespace")
|
||||
|
||||
inventoryList := &kkcorev1.InventoryList{}
|
||||
err := h.client.List(request.Request.Context(), inventoryList, &ctrlclient.ListOptions{Namespace: namespace, LabelSelector: queryParam.Selector(), FieldSelector: fieldselector})
|
||||
if err != nil {
|
||||
api.HandleError(response, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
results := query.DefaultList(inventoryList.Items, queryParam, func(left, right kkcorev1.Inventory, sortBy query.Field) bool {
|
||||
leftMeta, err := meta.Accessor(left)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
rightMeta, err := meta.Accessor(right)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return query.DefaultObjectMetaCompare(leftMeta, rightMeta, sortBy)
|
||||
}, func(o kkcorev1.Inventory, filter query.Filter) bool {
|
||||
// skip fieldselector
|
||||
if filter.Field == query.ParameterFieldSelector {
|
||||
return true
|
||||
}
|
||||
objectMeta, err := meta.Accessor(o)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return query.DefaultObjectMetaFilter(objectMeta, filter)
|
||||
})
|
||||
|
||||
_ = response.WriteEntity(results)
|
||||
}
|
||||
|
||||
// getInventory retrieves a specific inventory resource
|
||||
// It returns the inventory with the specified name in the given namespace
|
||||
func (h handler) inventoryInfo(request *restful.Request, response *restful.Response) {
|
||||
namespace := request.PathParameter("namespace")
|
||||
name := request.PathParameter("inventory")
|
||||
|
||||
inventory := &kkcorev1.Inventory{}
|
||||
|
||||
err := h.client.Get(request.Request.Context(), ctrlclient.ObjectKey{Namespace: namespace, Name: name}, inventory)
|
||||
if err != nil {
|
||||
api.HandleError(response, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
_ = response.WriteEntity(inventory)
|
||||
}
|
||||
|
||||
// listInventoryHosts lists all hosts in an inventory with their details
|
||||
// It includes information about SSH configuration, IP addresses, and group membership
|
||||
func (h handler) listInventoryHosts(request *restful.Request, response *restful.Response) {
|
||||
queryParam := query.ParseQueryParameter(request)
|
||||
namespace := request.PathParameter("namespace")
|
||||
name := request.PathParameter("inventory")
|
||||
|
||||
inventory := &kkcorev1.Inventory{}
|
||||
err := h.client.Get(request.Request.Context(), ctrlclient.ObjectKey{Namespace: namespace, Name: name}, inventory)
|
||||
if err != nil {
|
||||
api.HandleError(response, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
taskList := &kkcorev1alpha1.TaskList{}
|
||||
_ = h.client.List(request.Request.Context(), taskList, ctrlclient.InNamespace(namespace), ctrlclient.MatchingFields{
|
||||
"playbook.name": inventory.Annotations[kkcorev1.HostCheckPlaybookAnnotation],
|
||||
})
|
||||
|
||||
buildHostItem := func(hostname string, raw runtime.RawExtension) api.InventoryHostTable {
|
||||
vars := variable.Extension2Variables(raw)
|
||||
internalIPV4, _ := variable.StringVar(nil, vars, _const.VariableIPv4)
|
||||
internalIPV6, _ := variable.StringVar(nil, vars, _const.VariableIPv6)
|
||||
sshHost, _ := variable.StringVar(nil, vars, _const.VariableConnector, _const.VariableConnectorHost)
|
||||
sshPort, _ := variable.StringVar(nil, vars, _const.VariableConnector, _const.VariableConnectorPort)
|
||||
sshUser, _ := variable.StringVar(nil, vars, _const.VariableConnector, _const.VariableConnectorUser)
|
||||
sshPassword, _ := variable.StringVar(nil, vars, _const.VariableConnector, _const.VariableConnectorPassword)
|
||||
sshPrivateKey, _ := variable.StringVar(nil, vars, _const.VariableConnector, _const.VariableConnectorPrivateKey)
|
||||
|
||||
delete(vars, _const.VariableIPv4)
|
||||
delete(vars, _const.VariableIPv6)
|
||||
delete(vars, _const.VariableConnector)
|
||||
|
||||
return api.InventoryHostTable{
|
||||
Name: hostname,
|
||||
InternalIPV4: internalIPV4,
|
||||
InternalIPV6: internalIPV6,
|
||||
SSHHost: sshHost,
|
||||
SSHPort: sshPort,
|
||||
SSHUser: sshUser,
|
||||
SSHPassword: sshPassword,
|
||||
SSHPrivateKey: sshPrivateKey,
|
||||
Vars: vars,
|
||||
Groups: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
groups := variable.ConvertGroup(*inventory)
|
||||
fillGroups := func(item *api.InventoryHostTable) {
|
||||
for groupName, hosts := range groups {
|
||||
if groupName == _const.VariableGroupsAll || groupName == _const.VariableUnGrouped {
|
||||
continue
|
||||
}
|
||||
if slices.Contains(hosts, item.Name) {
|
||||
item.Groups = append(item.Groups, groupName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fillTaskInfo := func(item *api.InventoryHostTable) {
|
||||
for _, task := range taskList.Items {
|
||||
switch task.Name {
|
||||
case _const.Getenv(_const.TaskNameGatherFacts):
|
||||
for _, result := range task.Status.HostResults {
|
||||
if result.Host == item.Name {
|
||||
item.Status = result.Stdout
|
||||
break
|
||||
}
|
||||
}
|
||||
case _const.Getenv(_const.TaskNameGetArch):
|
||||
for _, result := range task.Status.HostResults {
|
||||
if result.Host == item.Name {
|
||||
item.Arch = result.Stdout
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
less := func(left, right api.InventoryHostTable, sortBy query.Field) bool {
|
||||
leftVal := left.Name
|
||||
if val := reflect.ValueOf(left).FieldByName(string(sortBy)); val.Kind() == reflect.String {
|
||||
leftVal = val.String()
|
||||
}
|
||||
rightVal := right.Name
|
||||
if val := reflect.ValueOf(right).FieldByName(string(sortBy)); val.Kind() == reflect.String {
|
||||
rightVal = val.String()
|
||||
}
|
||||
return leftVal > rightVal
|
||||
}
|
||||
|
||||
filter := func(o api.InventoryHostTable, f query.Filter) bool {
|
||||
if f.Field == query.ParameterFieldSelector {
|
||||
return true
|
||||
}
|
||||
objectMeta, err := meta.Accessor(o)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return query.DefaultObjectMetaFilter(objectMeta, f)
|
||||
}
|
||||
|
||||
hostTable := make([]api.InventoryHostTable, 0)
|
||||
for hostname, raw := range inventory.Spec.Hosts {
|
||||
item := buildHostItem(hostname, raw)
|
||||
fillGroups(&item)
|
||||
fillTaskInfo(&item)
|
||||
hostTable = append(hostTable, item)
|
||||
}
|
||||
|
||||
results := query.DefaultList(hostTable, queryParam, less, filter)
|
||||
_ = response.WriteEntity(results)
|
||||
}
|
||||
|
||||
// createPlaybook handles the creation of a new playbook resource
|
||||
func (h handler) createPlaybook(request *restful.Request, response *restful.Response) {
|
||||
playbook := &kkcorev1.Playbook{}
|
||||
if err := request.ReadEntity(playbook); err != nil {
|
||||
api.HandleBadRequest(response, request, err)
|
||||
return
|
||||
}
|
||||
// set workdir to playbook
|
||||
if err := unstructured.SetNestedField(playbook.Spec.Config.Value(), h.workdir, _const.Workdir); err != nil {
|
||||
api.HandleBadRequest(response, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.client.Create(request.Request.Context(), playbook); err != nil {
|
||||
api.HandleBadRequest(response, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
// create playbook log
|
||||
filename := filepath.Join(_const.GetWorkdirFromConfig(playbook.Spec.Config), _const.RuntimeDir, kkcorev1.SchemeGroupVersion.Group, kkcorev1.SchemeGroupVersion.Version, "playbooks", playbook.Namespace, playbook.Name, playbook.Name+".log")
|
||||
if _, err := os.Stat(filepath.Dir(filename)); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
api.HandleBadRequest(response, request, err)
|
||||
return
|
||||
}
|
||||
// if dir is not exist, create it.
|
||||
if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil {
|
||||
api.HandleBadRequest(response, request, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "failed to open file", "file", filename)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if err := executor.NewPlaybookExecutor(request.Request.Context(), h.client, playbook, file).Exec(request.Request.Context()); err != nil {
|
||||
klog.ErrorS(err, "failed to exec playbook", "playbook", playbook.Name)
|
||||
}
|
||||
}()
|
||||
|
||||
// for web ui. it not run in kubernetes. executor playbook right now
|
||||
_ = response.WriteEntity(playbook)
|
||||
}
|
||||
|
||||
// listPlaybooks handles listing playbook resources with filtering and pagination
|
||||
func (h handler) listPlaybooks(request *restful.Request, response *restful.Response) {
|
||||
queryParam := query.ParseQueryParameter(request)
|
||||
var fieldselector fields.Selector
|
||||
if v, ok := queryParam.Filters[query.ParameterFieldSelector]; ok {
|
||||
fs, err := fields.ParseSelector(string(v))
|
||||
if err != nil {
|
||||
api.HandleError(response, request, err)
|
||||
return
|
||||
}
|
||||
fieldselector = fs
|
||||
}
|
||||
namespace := request.PathParameter("namespace")
|
||||
|
||||
playbookList := &kkcorev1.PlaybookList{}
|
||||
err := h.client.List(request.Request.Context(), playbookList, &ctrlclient.ListOptions{Namespace: namespace, LabelSelector: queryParam.Selector(), FieldSelector: fieldselector})
|
||||
if err != nil {
|
||||
api.HandleError(response, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
results := query.DefaultList(playbookList.Items, queryParam, func(left, right kkcorev1.Playbook, sortBy query.Field) bool {
|
||||
leftMeta, err := meta.Accessor(left)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
rightMeta, err := meta.Accessor(right)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return query.DefaultObjectMetaCompare(leftMeta, rightMeta, sortBy)
|
||||
}, func(o kkcorev1.Playbook, filter query.Filter) bool {
|
||||
// skip fieldselector
|
||||
if filter.Field == query.ParameterFieldSelector {
|
||||
return true
|
||||
}
|
||||
objectMeta, err := meta.Accessor(o)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return query.DefaultObjectMetaFilter(objectMeta, filter)
|
||||
})
|
||||
|
||||
_ = response.WriteEntity(results)
|
||||
}
|
||||
|
||||
// playbookInfo handles retrieving a single playbook or watching for changes
|
||||
func (h handler) playbookInfo(request *restful.Request, response *restful.Response) {
|
||||
namespace := request.PathParameter("namespace")
|
||||
name := request.PathParameter("playbook")
|
||||
watch := request.QueryParameter("watch")
|
||||
|
||||
playbook := &kkcorev1.Playbook{}
|
||||
|
||||
if watch == "true" {
|
||||
h.restconfig.GroupVersion = &kkcorev1.SchemeGroupVersion
|
||||
client, err := rest.RESTClientFor(h.restconfig)
|
||||
if err != nil {
|
||||
api.HandleError(response, request, err)
|
||||
return
|
||||
}
|
||||
watchInterface, err := client.Get().Namespace(namespace).Resource("playbooks").Name(name).Param("watch", "true").Watch(request.Request.Context())
|
||||
if err != nil {
|
||||
api.HandleError(response, request, err)
|
||||
return
|
||||
}
|
||||
defer watchInterface.Stop()
|
||||
|
||||
response.AddHeader("Content-Type", "application/json")
|
||||
flusher, ok := response.ResponseWriter.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(response.ResponseWriter, "Streaming unsupported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(response.ResponseWriter)
|
||||
for event := range watchInterface.ResultChan() {
|
||||
if err := encoder.Encode(event.Object); err != nil {
|
||||
break
|
||||
}
|
||||
flusher.Flush()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
err := h.client.Get(request.Request.Context(), ctrlclient.ObjectKey{Namespace: namespace, Name: name}, playbook)
|
||||
if err != nil {
|
||||
api.HandleError(response, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
_ = response.WriteEntity(playbook)
|
||||
}
|
||||
|
||||
// logPlaybook handles streaming the log file for a playbook
|
||||
func (h handler) logPlaybook(request *restful.Request, response *restful.Response) {
|
||||
namespace := request.PathParameter("namespace")
|
||||
name := request.PathParameter("playbook")
|
||||
|
||||
playbook := &kkcorev1.Playbook{}
|
||||
err := h.client.Get(request.Request.Context(), ctrlclient.ObjectKey{Namespace: namespace, Name: name}, playbook)
|
||||
if err != nil {
|
||||
api.HandleError(response, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
filename := filepath.Join(_const.GetWorkdirFromConfig(playbook.Spec.Config), _const.RuntimeDir, kkcorev1.SchemeGroupVersion.Group, kkcorev1.SchemeGroupVersion.Version, "playbooks", playbook.Namespace, playbook.Name, playbook.Name+".log")
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
api.HandleError(response, request, err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
response.AddHeader("Content-Type", "text/plain; charset=utf-8")
|
||||
writer := response.ResponseWriter
|
||||
flusher, ok := writer.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(writer, "Streaming unsupported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(file)
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
fmt.Fprint(writer, line)
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
restfulspec "github.com/emicklei/go-restful-openapi/v2"
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
"github.com/go-openapi/spec"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"github.com/kubesphere/kubekey/v4/config"
|
||||
_const "github.com/kubesphere/kubekey/v4/pkg/const"
|
||||
"github.com/kubesphere/kubekey/v4/version"
|
||||
)
|
||||
|
||||
// NewSwaggerUIService creates a new WebService that serves the Swagger UI interface
|
||||
// It mounts the embedded swagger-ui files and handles requests to display the API documentation
|
||||
func NewSwaggerUIService() *restful.WebService {
|
||||
ws := new(restful.WebService)
|
||||
ws.Path("/swagger-ui")
|
||||
ws.Produces(restful.MIME_JSON)
|
||||
|
||||
subFS, err := fs.Sub(config.Swagger, "swagger-ui")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fileServer := http.StripPrefix("/swagger-ui/", http.FileServer(http.FS(subFS)))
|
||||
|
||||
ws.Route(ws.GET("").To(func(req *restful.Request, resp *restful.Response) {
|
||||
data, err := fs.ReadFile(subFS, "index.html")
|
||||
if err != nil {
|
||||
_ = resp.WriteError(http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
resp.AddHeader("Content-Type", "text/html")
|
||||
_, _ = resp.Write(data)
|
||||
}).Metadata(restfulspec.KeyOpenAPITags, []string{_const.OpenAPITag}))
|
||||
ws.Route(ws.GET("/{subpath:*}").To(func(req *restful.Request, resp *restful.Response) {
|
||||
fileServer.ServeHTTP(resp.ResponseWriter, req.Request)
|
||||
}).Metadata(restfulspec.KeyOpenAPITags, []string{_const.OpenAPITag}))
|
||||
|
||||
return ws
|
||||
}
|
||||
|
||||
// NewAPIService creates a new WebService that serves the OpenAPI/Swagger specification
|
||||
// It takes a list of WebServices and generates the API documentation
|
||||
func NewAPIService(webservice []*restful.WebService) *restful.WebService {
|
||||
restconfig := restfulspec.Config{
|
||||
WebServices: webservice, // you control what services are visible
|
||||
APIPath: "/apidocs.json",
|
||||
PostBuildSwaggerObjectHandler: enrichSwaggerObject}
|
||||
for _, ws := range webservice {
|
||||
klog.V(2).Infof("%s", ws.RootPath())
|
||||
}
|
||||
|
||||
return restfulspec.NewOpenAPIService(restconfig)
|
||||
}
|
||||
|
||||
// enrichSwaggerObject customizes the Swagger documentation with KubeKey-specific information
|
||||
// It sets the API title, description, version and contact information
|
||||
func enrichSwaggerObject(swo *spec.Swagger) {
|
||||
swo.Info = &spec.Info{
|
||||
InfoProps: spec.InfoProps{
|
||||
Title: "KubeKey Web API",
|
||||
Description: "KubeKey Web OpenAPI",
|
||||
Version: version.Get().String(),
|
||||
Contact: &spec.ContactInfo{
|
||||
ContactInfoProps: spec.ContactInfoProps{
|
||||
Name: "KubeKey",
|
||||
URL: "https://github.com/kubesphere/kubekey",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
Copyright 2020 The KubeSphere Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package query
|
||||
|
||||
// Field represents a query field name used for filtering and sorting
|
||||
type Field string
|
||||
|
||||
// Value represents a query field value used for filtering
|
||||
type Value string
|
||||
|
||||
const (
|
||||
// FieldName represents the name field of a resource
|
||||
FieldName = "name"
|
||||
// FieldNames represents multiple names field of resources
|
||||
FieldNames = "names"
|
||||
// FieldUID represents the unique identifier field of a resource
|
||||
FieldUID = "uid"
|
||||
// FieldCreationTimeStamp represents the creation timestamp field of a resource
|
||||
FieldCreationTimeStamp = "creationTimestamp"
|
||||
// FieldCreateTime represents the creation time field of a resource
|
||||
FieldCreateTime = "createTime"
|
||||
// FieldLastUpdateTimestamp represents the last update timestamp field of a resource
|
||||
FieldLastUpdateTimestamp = "lastUpdateTimestamp"
|
||||
// FieldUpdateTime represents the update time field of a resource
|
||||
FieldUpdateTime = "updateTime"
|
||||
// FieldLabel represents the label field of a resource
|
||||
FieldLabel = "label"
|
||||
// FieldAnnotation represents the annotation field of a resource
|
||||
FieldAnnotation = "annotation"
|
||||
// FieldNamespace represents the namespace field of a resource
|
||||
FieldNamespace = "namespace"
|
||||
// FieldStatus represents the status field of a resource
|
||||
FieldStatus = "status"
|
||||
// FieldOwnerReference represents the owner reference field of a resource
|
||||
FieldOwnerReference = "ownerReference"
|
||||
// FieldOwnerKind represents the owner kind field of a resource
|
||||
FieldOwnerKind = "ownerKind"
|
||||
)
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
Copyright 2020 KubeSphere Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package query
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"github.com/kubesphere/kubekey/v4/pkg/web/api"
|
||||
)
|
||||
|
||||
// CompareFunc is a generic function type that compares two objects of type T
|
||||
// Returns true if left is greater than right
|
||||
type CompareFunc[T any] func(T, T, Field) bool
|
||||
|
||||
// FilterFunc is a generic function type that filters objects of type T
|
||||
// Returns true if the object matches the filter criteria
|
||||
type FilterFunc[T any] func(T, Filter) bool
|
||||
|
||||
// TransformFunc is a generic function type that transforms objects of type T
|
||||
// Returns the transformed object
|
||||
type TransformFunc[T any] func(T) T
|
||||
|
||||
// DefaultList processes a list of objects with filtering, sorting, and pagination
|
||||
// Parameters:
|
||||
// - objects: The list of objects to process
|
||||
// - q: Query parameters including filters, sorting, and pagination
|
||||
// - compareFunc: Function to compare objects for sorting
|
||||
// - filterFunc: Function to filter objects
|
||||
// - transformFuncs: Optional functions to transform objects
|
||||
//
|
||||
// Returns a ListResult containing the processed objects
|
||||
func DefaultList[T any](objects []T, q *Query, compareFunc CompareFunc[T], filterFunc FilterFunc[T], transformFuncs ...TransformFunc[T]) *api.ListResult[T] {
|
||||
// selected matched ones
|
||||
filtered := make([]T, 0)
|
||||
for _, object := range objects {
|
||||
selected := true
|
||||
for field, value := range q.Filters {
|
||||
if !filterFunc(object, Filter{Field: field, Value: value}) {
|
||||
selected = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if selected {
|
||||
for _, transform := range transformFuncs {
|
||||
object = transform(object)
|
||||
}
|
||||
filtered = append(filtered, object)
|
||||
}
|
||||
}
|
||||
|
||||
// sort by sortBy field
|
||||
sort.Slice(filtered, func(i, j int) bool {
|
||||
if !q.Ascending {
|
||||
return compareFunc(filtered[i], filtered[j], q.SortBy)
|
||||
}
|
||||
return !compareFunc(filtered[i], filtered[j], q.SortBy)
|
||||
})
|
||||
|
||||
total := len(filtered)
|
||||
|
||||
if q.Pagination == nil {
|
||||
q.Pagination = NoPagination
|
||||
}
|
||||
|
||||
start, end := q.Pagination.GetValidPagination(total)
|
||||
|
||||
return &api.ListResult[T]{
|
||||
TotalItems: len(filtered),
|
||||
Items: filtered[start:end],
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultObjectMetaCompare compares two metav1.Object instances
|
||||
// Returns true if left is greater than right based on the specified sort field
|
||||
// Supports sorting by name or creation timestamp
|
||||
func DefaultObjectMetaCompare(left, right metav1.Object, sortBy Field) bool {
|
||||
switch sortBy {
|
||||
// ?sortBy=name
|
||||
case FieldName:
|
||||
return left.GetName() > right.GetName()
|
||||
// ?sortBy=creationTimestamp
|
||||
default:
|
||||
// compare by name if creation timestamp is equal
|
||||
if left.GetCreationTimestamp().Time.Equal(right.GetCreationTimestamp().Time) {
|
||||
return left.GetName() > right.GetName()
|
||||
}
|
||||
return left.GetCreationTimestamp().After(right.GetCreationTimestamp().Time)
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultObjectMetaFilter filters metav1.Object instances based on various criteria
|
||||
// Supports filtering by:
|
||||
// - Names: Exact match against a comma-separated list of names
|
||||
// - Name: Partial match against object name
|
||||
// - UID: Exact match against object UID
|
||||
// - Namespace: Exact match against namespace
|
||||
// - OwnerReference: Match against owner reference UID
|
||||
// - OwnerKind: Match against owner reference kind
|
||||
// - Annotation: Match against annotations using label selector syntax
|
||||
// - Label: Match against labels using label selector syntax
|
||||
func DefaultObjectMetaFilter(item metav1.Object, filter Filter) bool {
|
||||
switch filter.Field {
|
||||
case FieldNames:
|
||||
for _, name := range strings.Split(string(filter.Value), ",") {
|
||||
if item.GetName() == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
// /namespaces?page=1&limit=10&name=default
|
||||
case FieldName:
|
||||
return strings.Contains(item.GetName(), string(filter.Value))
|
||||
// /namespaces?page=1&limit=10&uid=a8a8d6cf-f6a5-4fea-9c1b-e57610115706
|
||||
case FieldUID:
|
||||
return string(item.GetUID()) == string(filter.Value)
|
||||
// /deployments?page=1&limit=10&namespace=kubesphere-system
|
||||
case FieldNamespace:
|
||||
return item.GetNamespace() == string(filter.Value)
|
||||
// /namespaces?page=1&limit=10&ownerReference=a8a8d6cf-f6a5-4fea-9c1b-e57610115706
|
||||
case FieldOwnerReference:
|
||||
for _, ownerReference := range item.GetOwnerReferences() {
|
||||
if string(ownerReference.UID) == string(filter.Value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
// /namespaces?page=1&limit=10&ownerKind=Workspace
|
||||
case FieldOwnerKind:
|
||||
for _, ownerReference := range item.GetOwnerReferences() {
|
||||
if ownerReference.Kind == string(filter.Value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
// /namespaces?page=1&limit=10&annotation=openpitrix_runtime
|
||||
case FieldAnnotation:
|
||||
return labelMatch(item.GetAnnotations(), string(filter.Value))
|
||||
// /namespaces?page=1&limit=10&label=kubesphere.io/workspace:system-workspace
|
||||
case FieldLabel:
|
||||
return labelMatch(item.GetLabels(), string(filter.Value))
|
||||
// not supported filter
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// labelMatch checks if a map of labels/annotations matches a label selector
|
||||
// Returns true if the map matches the selector, false otherwise
|
||||
func labelMatch(m map[string]string, filter string) bool {
|
||||
labelSelector, err := labels.Parse(filter)
|
||||
if err != nil {
|
||||
klog.Warningf("invalid labelSelector %s: %s", filter, err)
|
||||
return false
|
||||
}
|
||||
return labelSelector.Matches(labels.Set(m))
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
Copyright 2020 The KubeSphere Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package query
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
)
|
||||
|
||||
// Common query parameter names used in API requests
|
||||
const (
|
||||
ParameterName = "name" // Name parameter for filtering by resource name
|
||||
ParameterLabelSelector = "labelSelector" // Label selector parameter for filtering by labels
|
||||
ParameterFieldSelector = "fieldSelector" // Field selector parameter for filtering by fields
|
||||
ParameterPage = "page" // Page number parameter for pagination
|
||||
ParameterLimit = "limit" // Items per page parameter for pagination
|
||||
ParameterOrderBy = "sortBy" // Field to sort results by
|
||||
ParameterAscending = "ascending" // Sort direction parameter (true for ascending, false for descending)
|
||||
)
|
||||
|
||||
// Query represents API search terms and filtering options
|
||||
type Query struct {
|
||||
Pagination *Pagination // Pagination settings for the query results
|
||||
|
||||
// SortBy specifies which field to sort results by, defaults to FieldCreationTimeStamp
|
||||
SortBy Field
|
||||
|
||||
// Ascending determines sort direction, defaults to descending (false)
|
||||
Ascending bool
|
||||
|
||||
// Filters contains field-value pairs for filtering results
|
||||
Filters map[Field]Value
|
||||
|
||||
// LabelSelector contains the label selector string for filtering by labels
|
||||
LabelSelector string
|
||||
}
|
||||
|
||||
// Pagination represents pagination settings for query results
|
||||
type Pagination struct {
|
||||
Limit int // Number of items per page
|
||||
Offset int // Starting offset for the current page
|
||||
}
|
||||
|
||||
// NoPagination represents a query without pagination
|
||||
var NoPagination = newPagination(-1, 0)
|
||||
|
||||
// newPagination creates a new Pagination instance with the given limit and offset
|
||||
func newPagination(limit int, offset int) *Pagination {
|
||||
return &Pagination{
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
}
|
||||
}
|
||||
|
||||
// Selector returns a labels.Selector for the query's label selector
|
||||
func (q *Query) Selector() labels.Selector {
|
||||
selector, err := labels.Parse(q.LabelSelector)
|
||||
if err != nil {
|
||||
return labels.Everything()
|
||||
}
|
||||
|
||||
return selector
|
||||
}
|
||||
|
||||
// AppendLabelSelector adds additional label selectors to the existing query
|
||||
func (q *Query) AppendLabelSelector(ls map[string]string) error {
|
||||
labelsMap, err := labels.ConvertSelectorToLabelsMap(q.LabelSelector)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q.LabelSelector = labels.Merge(labelsMap, ls).String()
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetValidPagination calculates valid start and end indices for pagination
|
||||
func (p *Pagination) GetValidPagination(total int) (startIndex, endIndex int) {
|
||||
// Return all items if no pagination is specified
|
||||
if p.Limit == NoPagination.Limit {
|
||||
return 0, total
|
||||
}
|
||||
|
||||
// Return empty range if pagination parameters are invalid
|
||||
if p.Limit < 0 || p.Offset < 0 || p.Offset > total {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
startIndex = p.Offset
|
||||
endIndex = startIndex + p.Limit
|
||||
|
||||
// Adjust end index if it exceeds total items
|
||||
if endIndex > total {
|
||||
endIndex = total
|
||||
}
|
||||
|
||||
return startIndex, endIndex
|
||||
}
|
||||
|
||||
// New creates a new Query instance with default values
|
||||
func New() *Query {
|
||||
return &Query{
|
||||
Pagination: NoPagination,
|
||||
SortBy: "",
|
||||
Ascending: false,
|
||||
Filters: map[Field]Value{},
|
||||
}
|
||||
}
|
||||
|
||||
// Filter represents a single field-value filter pair
|
||||
type Filter struct {
|
||||
Field Field `json:"field"` // Field to filter on
|
||||
Value Value `json:"value"` // Value to filter by
|
||||
}
|
||||
|
||||
// ParseQueryParameter parses query parameters from a RESTful request into a Query struct
|
||||
func ParseQueryParameter(request *restful.Request) *Query {
|
||||
query := New()
|
||||
|
||||
// Parse pagination parameters
|
||||
limit, err := strconv.Atoi(request.QueryParameter(ParameterLimit))
|
||||
if err != nil {
|
||||
limit = -1 // Use default value if undefined
|
||||
}
|
||||
page, err := strconv.Atoi(request.QueryParameter(ParameterPage))
|
||||
if err != nil {
|
||||
page = 1 // Use default value if undefined
|
||||
}
|
||||
|
||||
query.Pagination = newPagination(limit, (page-1)*limit)
|
||||
|
||||
// Parse sorting parameters
|
||||
query.SortBy = Field(defaultString(request.QueryParameter(ParameterOrderBy), FieldCreationTimeStamp))
|
||||
|
||||
ascending, err := strconv.ParseBool(defaultString(request.QueryParameter(ParameterAscending), "false"))
|
||||
if err != nil {
|
||||
query.Ascending = false
|
||||
} else {
|
||||
query.Ascending = ascending
|
||||
}
|
||||
|
||||
// Parse label selector
|
||||
query.LabelSelector = request.QueryParameter(ParameterLabelSelector)
|
||||
|
||||
// Parse additional filters
|
||||
for key, values := range request.Request.URL.Query() {
|
||||
if !slices.Contains([]string{ParameterPage, ParameterLimit, ParameterOrderBy, ParameterAscending, ParameterLabelSelector}, key) {
|
||||
value := ""
|
||||
if len(values) > 0 {
|
||||
value = values[0]
|
||||
}
|
||||
query.Filters[Field(key)] = Value(value)
|
||||
}
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
// defaultString returns the default value if the input string is empty
|
||||
func defaultString(value, defaultValue string) string {
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
Copyright 2020 The KubeSphere Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestParseQueryParameter(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
queryString string
|
||||
expected *Query
|
||||
}{
|
||||
{
|
||||
"test normal case",
|
||||
"label=app.kubernetes.io/name=book&name=foo&status=Running&page=1&limit=10&ascending=true",
|
||||
&Query{
|
||||
Pagination: newPagination(10, 0),
|
||||
SortBy: FieldCreationTimeStamp,
|
||||
Ascending: true,
|
||||
Filters: map[Field]Value{
|
||||
FieldLabel: Value("app.kubernetes.io/name=book"),
|
||||
FieldName: Value("foo"),
|
||||
FieldStatus: Value("Running"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"test bad case",
|
||||
"xxxx=xxxx&dsfsw=xxxx&page=abc&limit=add&ascending=ssss",
|
||||
&Query{
|
||||
Pagination: NoPagination,
|
||||
SortBy: FieldCreationTimeStamp,
|
||||
Ascending: false,
|
||||
Filters: map[Field]Value{
|
||||
Field("xxxx"): Value("xxxx"),
|
||||
Field("dsfsw"): Value("xxxx"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://localhost?"+test.queryString, http.NoBody)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
request := restful.NewRequest(req)
|
||||
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
got := ParseQueryParameter(request)
|
||||
|
||||
if diff := cmp.Diff(got, test.expected); diff != "" {
|
||||
t.Errorf("%T differ (-got, +want): %s", test.expected, diff)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
restfulspec "github.com/emicklei/go-restful-openapi/v2"
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
kkcorev1 "github.com/kubesphere/kubekey/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/rest"
|
||||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
_const "github.com/kubesphere/kubekey/v4/pkg/const"
|
||||
"github.com/kubesphere/kubekey/v4/pkg/web/api"
|
||||
"github.com/kubesphere/kubekey/v4/pkg/web/query"
|
||||
)
|
||||
|
||||
// NewWebService creates and configures a new RESTful web service for managing inventories and playbooks.
|
||||
// It sets up routes for CRUD operations on inventories and playbooks, including pagination, sorting, and filtering.
|
||||
// Parameters:
|
||||
// - ctx: Context for the web service
|
||||
// - workdir: Working directory for file operations
|
||||
// - client: Kubernetes controller client
|
||||
// - config: REST configuration
|
||||
//
|
||||
// Returns a configured WebService instance
|
||||
func NewWebService(ctx context.Context, workdir string, client ctrlclient.Client, config *rest.Config) *restful.WebService {
|
||||
ws := new(restful.WebService)
|
||||
// the GroupVersion might be empty, we need to remove the final /
|
||||
ws.Path(strings.TrimRight(_const.APIPath+kkcorev1.SchemeGroupVersion.String(), "/")).
|
||||
Produces(restful.MIME_JSON).Consumes(
|
||||
string(types.JSONPatchType),
|
||||
string(types.MergePatchType),
|
||||
string(types.StrategicMergePatchType),
|
||||
string(types.ApplyPatchType),
|
||||
restful.MIME_JSON)
|
||||
|
||||
h := newHandler(workdir, client, config)
|
||||
|
||||
// Inventory management routes
|
||||
ws.Route(ws.POST("/inventories").To(h.createInventory).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
||||
Doc("create a inventory.").
|
||||
Reads(kkcorev1.Inventory{}).
|
||||
Returns(http.StatusOK, _const.StatusOK, kkcorev1.Inventory{}))
|
||||
|
||||
ws.Route(ws.PATCH("/namespaces/{namespace}/inventories/{inventory}").To(h.patchInventory).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
||||
Doc("patch a inventory.").
|
||||
Param(ws.PathParameter("namespace", "the namespace of the inventory")).
|
||||
Param(ws.PathParameter("inventory", "the name of the inventory")).
|
||||
Reads(kkcorev1.Inventory{}).
|
||||
Returns(http.StatusOK, _const.StatusOK, kkcorev1.Inventory{}))
|
||||
|
||||
ws.Route(ws.GET("/inventories").To(h.listInventories).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
||||
Doc("list all inventories.").
|
||||
Param(ws.QueryParameter(query.ParameterPage, "page").Required(false).DataFormat("page=%d").DefaultValue("page=1")).
|
||||
Param(ws.QueryParameter(query.ParameterLimit, "limit").Required(false)).
|
||||
Param(ws.QueryParameter(query.ParameterAscending, "sort parameters, e.g. reverse=true").Required(false).DefaultValue("ascending=false")).
|
||||
Param(ws.QueryParameter(query.ParameterOrderBy, "sort parameters, e.g. orderBy=createTime")).
|
||||
Returns(http.StatusOK, _const.StatusOK, api.ListResult[kkcorev1.Inventory]{}))
|
||||
|
||||
ws.Route(ws.GET("/namespaces/{namespace}/inventories").To(h.listInventories).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
||||
Doc("list all inventories in a namespace.").
|
||||
Param(ws.PathParameter("namespace", "the namespace of the inventory")).
|
||||
Param(ws.QueryParameter(query.ParameterPage, "page").Required(false).DataFormat("page=%d").DefaultValue("page=1")).
|
||||
Param(ws.QueryParameter(query.ParameterLimit, "limit").Required(false)).
|
||||
Param(ws.QueryParameter(query.ParameterAscending, "sort parameters, e.g. reverse=true").Required(false).DefaultValue("ascending=false")).
|
||||
Param(ws.QueryParameter(query.ParameterOrderBy, "sort parameters, e.g. orderBy=createTime")).
|
||||
Returns(http.StatusOK, _const.StatusOK, api.ListResult[kkcorev1.Inventory]{}))
|
||||
|
||||
ws.Route(ws.GET("/namespaces/{namespace}/inventories/{inventory}").To(h.inventoryInfo).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
||||
Doc("get a inventory in a namespace.").
|
||||
Param(ws.PathParameter("namespace", "the namespace of the inventory")).
|
||||
Param(ws.PathParameter("inventory", "the name of the inventory")).
|
||||
Returns(http.StatusOK, _const.StatusOK, kkcorev1.Inventory{}))
|
||||
|
||||
ws.Route(ws.GET("/namespaces/{namespace}/inventories/{inventory}/hosts").To(h.listInventoryHosts).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
||||
Doc("list all hosts in a inventory.").
|
||||
Param(ws.PathParameter("namespace", "the namespace of the inventory")).
|
||||
Param(ws.PathParameter("inventory", "the name of the inventory")).
|
||||
Param(ws.QueryParameter(query.ParameterPage, "page").Required(false).DataFormat("page=%d").DefaultValue("page=1")).
|
||||
Param(ws.QueryParameter(query.ParameterLimit, "limit").Required(false)).
|
||||
Param(ws.QueryParameter(query.ParameterAscending, "sort parameters, e.g. reverse=true").Required(false).DefaultValue("ascending=false")).
|
||||
Param(ws.QueryParameter(query.ParameterOrderBy, "sort parameters, e.g. orderBy=createTime")).
|
||||
Returns(http.StatusOK, _const.StatusOK, api.ListResult[api.InventoryHostTable]{}))
|
||||
|
||||
// Playbook management routes
|
||||
ws.Route(ws.POST("/playbooks").To(h.createPlaybook).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
||||
Doc("create a playbook.").
|
||||
Reads(kkcorev1.Playbook{}).
|
||||
Returns(http.StatusOK, _const.StatusOK, kkcorev1.Playbook{}))
|
||||
|
||||
ws.Route(ws.GET("/playbooks").To(h.listPlaybooks).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
||||
Doc("list all playbooks.").
|
||||
Reads(kkcorev1.Playbook{}).
|
||||
Param(ws.QueryParameter(query.ParameterPage, "page").Required(false).DataFormat("page=%d").DefaultValue("page=1")).
|
||||
Param(ws.QueryParameter(query.ParameterLimit, "limit").Required(false)).
|
||||
Param(ws.QueryParameter(query.ParameterAscending, "sort parameters, e.g. reverse=true").Required(false).DefaultValue("ascending=false")).
|
||||
Param(ws.QueryParameter(query.ParameterOrderBy, "sort parameters, e.g. orderBy=createTime")).
|
||||
Returns(http.StatusOK, _const.StatusOK, api.ListResult[kkcorev1.Playbook]{}))
|
||||
|
||||
ws.Route(ws.GET("/namespaces/{namespace}/playbooks").To(h.listInventories).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
||||
Doc("list all playbooks in a namespace.").
|
||||
Param(ws.PathParameter("namespace", "the namespace of the playbook")).
|
||||
Param(ws.QueryParameter(query.ParameterPage, "page").Required(false).DataFormat("page=%d").DefaultValue("page=1")).
|
||||
Param(ws.QueryParameter(query.ParameterLimit, "limit").Required(false)).
|
||||
Param(ws.QueryParameter(query.ParameterAscending, "sort parameters, e.g. reverse=true").Required(false).DefaultValue("ascending=false")).
|
||||
Param(ws.QueryParameter(query.ParameterOrderBy, "sort parameters, e.g. orderBy=createTime")).
|
||||
Returns(http.StatusOK, _const.StatusOK, api.ListResult[kkcorev1.Playbook]{}))
|
||||
|
||||
ws.Route(ws.GET("/namespaces/{namespace}/playbooks/{playbook}").To(h.playbookInfo).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
||||
Doc("get or watch a playbook in a namespace.").
|
||||
Param(ws.PathParameter("namespace", "the namespace of the playbook")).
|
||||
Param(ws.PathParameter("playbook", "the name of the playbook")).
|
||||
Param(ws.QueryParameter("watch", "set to true to watch this playbook")).
|
||||
Returns(http.StatusOK, _const.StatusOK, kkcorev1.Playbook{}))
|
||||
|
||||
ws.Route(ws.GET("/namespaces/{namespace}/playbooks/{playbook}/log").To(h.logPlaybook).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
||||
Doc("watch a playbook in a namespace.").
|
||||
Param(ws.PathParameter("namespace", "the namespace of the playbook")).
|
||||
Param(ws.PathParameter("playbook", "the name of the playbook")).
|
||||
Returns(http.StatusOK, _const.StatusOK, "text/plain"))
|
||||
|
||||
return ws
|
||||
}
|
||||
Loading…
Reference in New Issue