diff --git a/go.mod b/go.mod index 3fb0b979a..3c931a8ef 100644 --- a/go.mod +++ b/go.mod @@ -102,6 +102,8 @@ require ( github.com/MichaelMure/go-term-text v0.3.1 // indirect github.com/alecthomas/chroma v0.10.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect + github.com/antchfx/jsonquery v1.3.6 // indirect + github.com/antchfx/xpath v1.3.2 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect @@ -124,6 +126,7 @@ require ( github.com/go-openapi/analysis v0.23.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 // indirect github.com/gorilla/schema v1.4.1 // indirect @@ -137,6 +140,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/josharian/native v1.1.0 // indirect + github.com/judedaryl/go-arrayutils v0.0.1 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kr/pty v1.1.8 // indirect github.com/kyokomi/emoji/v2 v2.2.12 // indirect diff --git a/go.sum b/go.sum index 6aa4a4f15..51ecae9ac 100644 --- a/go.sum +++ b/go.sum @@ -80,6 +80,10 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/antchfx/jsonquery v1.3.6 h1:TaSfeAh7n6T11I74bsZ1FswreIfrbJ0X+OyLflx6mx4= +github.com/antchfx/jsonquery v1.3.6/go.mod h1:fGzSGJn9Y826Qd3pC8Wx45avuUwpkePsACQJYy+58BU= +github.com/antchfx/xpath v1.3.2 h1:LNjzlsSjinu3bQpw9hWMY9ocB80oLOWuQqFvO6xt51U= +github.com/antchfx/xpath v1.3.2/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= @@ -256,6 +260,8 @@ github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -427,6 +433,8 @@ github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/judedaryl/go-arrayutils v0.0.1 h1:89rWXRVp1c1gcE1UEWvFuohVMeYwfA0y4TMZtE8dS58= +github.com/judedaryl/go-arrayutils v0.0.1/go.mod h1:vqtnlEkOBpDGHS3U3kQtMJZGTOC+SBFAQYj2KcxLf1A= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kataras/go-events v0.0.3 h1:o5YK53uURXtrlg7qE/vovxd/yKOJcLuFtPQbf1rYMC4= github.com/kataras/go-events v0.0.3/go.mod h1:bFBgtzwwzrag7kQmGuU1ZaVxhK2qseYPQomXoVEMsj4= @@ -610,6 +618,8 @@ github.com/parallaxsecond/parsec-client-go v0.0.0-20221025095442-f0a77d263cf9/go github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= diff --git a/internal/ascode/cache_util.go b/internal/ascode/cache_util.go new file mode 100644 index 000000000..775670877 --- /dev/null +++ b/internal/ascode/cache_util.go @@ -0,0 +1,50 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 ascode + +import ( + "errors" + "github.com/michaelquigley/pfxlog" + "reflect" +) + +type CacheGetter func(id string) (interface{}, error) + +var log = pfxlog.Logger() + +func GetItemFromCache(c map[string]interface{}, key string, fn CacheGetter) (interface{}, error) { + if key == "" { + return nil, errors.New("key is null, can't resolve from cache or get it from source") + } + detail, found := c[key] + if !found { + log.WithFields(map[string]interface{}{"key": key}).Debug("Item not in cache, getting from source") + var err error + detail, err = fn(key) + if err != nil { + log.WithFields(map[string]interface{}{"key": key}).WithError(err).Debug("Error reading from source, returning error") + return nil, errors.Join(errors.New("error reading: "+key), err) + } + if detail != nil && !reflect.ValueOf(detail).IsNil() { + log.WithFields(map[string]interface{}{"key": key, "item": detail}).Debug("Item read from source, caching") + c[key] = detail + } + return detail, nil + } + log.WithFields(map[string]interface{}{"key": key}).Debug("Item found in cache") + return detail, nil +} diff --git a/ziti/cmd/verify/common.go b/internal/log_format.go similarity index 95% rename from ziti/cmd/verify/common.go rename to internal/log_format.go index 3f1384c0c..f9d76df30 100644 --- a/ziti/cmd/verify/common.go +++ b/internal/log_format.go @@ -13,14 +13,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package verify + +package internal import ( "github.com/sirupsen/logrus" "runtime" ) -func configureLogFormat(level logrus.Level) { +func ConfigureLogFormat(level logrus.Level) { logrus.SetLevel(level) logrus.SetFormatter(&logrus.TextFormatter{ ForceColors: true, diff --git a/internal/print.go b/internal/print.go new file mode 100644 index 000000000..b54a5ca4c --- /dev/null +++ b/internal/print.go @@ -0,0 +1,14 @@ +package internal + +import "fmt" +import "io" + +/* +Extends the standard FPrintF with overwriting the current line because it has the `\u001B[2K` +*/ +func FPrintfReusingLine(writer io.Writer, format string, a ...any) (n int, err error) { + return fmt.Fprintf(writer, "\u001B[2K"+format+"\r", a...) +} +func FPrintflnReusingLine(writer io.Writer, format string, a ...any) (n int, err error) { + return FPrintfReusingLine(writer, format+"\n", a...) +} diff --git a/internal/rest/mgmt/helpers.go b/internal/rest/mgmt/helpers.go index 006a0871b..128f49838 100644 --- a/internal/rest/mgmt/helpers.go +++ b/internal/rest/mgmt/helpers.go @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ + package mgmt import ( @@ -20,10 +21,19 @@ import ( "crypto/tls" "crypto/x509" "errors" + "fmt" "github.com/openziti/edge-api/rest_management_api_client" + "github.com/openziti/edge-api/rest_management_api_client/auth_policy" + "github.com/openziti/edge-api/rest_management_api_client/certificate_authority" + "github.com/openziti/edge-api/rest_management_api_client/config" rest_mgmt "github.com/openziti/edge-api/rest_management_api_client/current_api_session" + "github.com/openziti/edge-api/rest_management_api_client/edge_router" + "github.com/openziti/edge-api/rest_management_api_client/edge_router_policy" + "github.com/openziti/edge-api/rest_management_api_client/external_jwt_signer" "github.com/openziti/edge-api/rest_management_api_client/identity" + "github.com/openziti/edge-api/rest_management_api_client/posture_checks" "github.com/openziti/edge-api/rest_management_api_client/service" + "github.com/openziti/edge-api/rest_management_api_client/service_edge_router_policy" "github.com/openziti/edge-api/rest_management_api_client/service_policy" "github.com/openziti/edge-api/rest_model" "github.com/openziti/edge-api/rest_util" @@ -34,12 +44,16 @@ import ( "time" ) +const ( + DefaultTimeout = 5 * time.Second +) + func IdentityFromFilter(client *rest_management_api_client.ZitiEdgeManagement, filter string) *rest_model.IdentityDetail { params := &identity.ListIdentitiesParams{ Filter: &filter, Context: context.Background(), } - params.SetTimeout(5 * time.Second) + params.SetTimeout(DefaultTimeout) resp, err := client.Identity.ListIdentities(params, nil) if err != nil { log.Debugf("Could not obtain an ID for the identity with filter %s: %v", filter, err) @@ -57,7 +71,7 @@ func ServiceFromFilter(client *rest_management_api_client.ZitiEdgeManagement, fi Filter: &filter, Context: context.Background(), } - params.SetTimeout(5 * time.Second) + params.SetTimeout(DefaultTimeout) resp, err := client.Service.ListServices(params, nil) if err != nil { log.Debugf("Could not obtain an ID for the service with filter %s: %v", filter, err) @@ -74,10 +88,160 @@ func ServicePolicyFromFilter(client *rest_management_api_client.ZitiEdgeManageme Filter: &filter, Context: context.Background(), } - params.SetTimeout(5 * time.Second) + params.SetTimeout(DefaultTimeout) resp, err := client.ServicePolicy.ListServicePolicies(params, nil) if err != nil { - log.Errorf("Could not obtain an ID for the service with filter %s: %v", filter, err) + log.Errorf("Could not obtain an ID for the service policy with filter %s: %v", filter, err) + return nil + } + if resp == nil || resp.Payload == nil || resp.Payload.Data == nil || len(resp.Payload.Data) == 0 { + return nil + } + return resp.Payload.Data[0] +} + +func AuthPolicyFromFilter(client *rest_management_api_client.ZitiEdgeManagement, filter string) *rest_model.AuthPolicyDetail { + params := &auth_policy.ListAuthPoliciesParams{ + Filter: &filter, + Context: context.Background(), + } + params.SetTimeout(DefaultTimeout) + resp, err := client.AuthPolicy.ListAuthPolicies(params, nil) + if err != nil { + log.Errorf("Could not obtain an ID for the auth policy with filter %s: %v", filter, err) + return nil + } + if resp == nil || resp.Payload == nil || resp.Payload.Data == nil || len(resp.Payload.Data) == 0 { + return nil + } + return resp.Payload.Data[0] +} + +func CertificateAuthorityFromFilter(client *rest_management_api_client.ZitiEdgeManagement, filter string) *rest_model.CaDetail { + params := &certificate_authority.ListCasParams{ + Filter: &filter, + Context: context.Background(), + } + params.SetTimeout(DefaultTimeout) + resp, err := client.CertificateAuthority.ListCas(params, nil) + if err != nil { + log.Errorf("Could not obtain an ID for the certificate authority with filter %s: %v", filter, err) + return nil + } + if resp == nil || resp.Payload == nil || resp.Payload.Data == nil || len(resp.Payload.Data) == 0 { + return nil + } + return resp.Payload.Data[0] +} + +func ConfigTypeFromFilter(client *rest_management_api_client.ZitiEdgeManagement, filter string) *rest_model.ConfigTypeDetail { + params := &config.ListConfigTypesParams{ + Filter: &filter, + Context: context.Background(), + } + params.SetTimeout(DefaultTimeout) + resp, err := client.Config.ListConfigTypes(params, nil) + if err != nil { + log.Errorf("Could not obtain an ID for the config type with filter %s: %v", filter, err) + return nil + } + if resp == nil || resp.Payload == nil || resp.Payload.Data == nil || len(resp.Payload.Data) == 0 { + return nil + } + return resp.Payload.Data[0] +} + +func ConfigFromFilter(client *rest_management_api_client.ZitiEdgeManagement, filter string) *rest_model.ConfigDetail { + params := &config.ListConfigsParams{ + Filter: &filter, + Context: context.Background(), + } + params.SetTimeout(DefaultTimeout) + resp, err := client.Config.ListConfigs(params, nil) + if err != nil { + log.Errorf("Could not obtain an ID for the config with filter %s: %v", filter, err) + return nil + } + if resp == nil || resp.Payload == nil || resp.Payload.Data == nil || len(resp.Payload.Data) == 0 { + return nil + } + return resp.Payload.Data[0] +} + +func ExternalJWTSignerFromFilter(client *rest_management_api_client.ZitiEdgeManagement, filter string) *rest_model.ExternalJWTSignerDetail { + params := &external_jwt_signer.ListExternalJWTSignersParams{ + Filter: &filter, + Context: context.Background(), + } + params.SetTimeout(DefaultTimeout) + resp, err := client.ExternalJWTSigner.ListExternalJWTSigners(params, nil) + if err != nil { + log.Errorf("Could not obtain an ID for the external jwt signer with filter %s: %v", filter, err) + return nil + } + if resp == nil || resp.Payload == nil || resp.Payload.Data == nil || len(resp.Payload.Data) == 0 { + return nil + } + return resp.Payload.Data[0] +} + +func PostureCheckFromFilter(client *rest_management_api_client.ZitiEdgeManagement, filter string) *rest_model.PostureCheckDetail { + params := &posture_checks.ListPostureChecksParams{ + Filter: &filter, + Context: context.Background(), + } + params.SetTimeout(DefaultTimeout) + resp, err := client.PostureChecks.ListPostureChecks(params, nil) + if err != nil { + log.Errorf("Could not obtain an ID for the posture check with filter %s: %v", filter, err) + return nil + } + if resp == nil || resp.Payload == nil || len(resp.Payload.Data()) == 0 { + return nil + } + return &resp.Payload.Data()[0] +} + +func EdgeRouterPolicyFromFilter(client *rest_management_api_client.ZitiEdgeManagement, filter string) *rest_model.EdgeRouterPolicyDetail { + params := &edge_router_policy.ListEdgeRouterPoliciesParams{ + Filter: &filter, + } + params.SetTimeout(DefaultTimeout) + resp, err := client.EdgeRouterPolicy.ListEdgeRouterPolicies(params, nil) + if err != nil { + log.Errorf("Could not obtain an ID for the edge router policies with filter %s: %v", filter, err) + return nil + } + if resp == nil || resp.Payload == nil || resp.Payload.Data == nil || len(resp.Payload.Data) == 0 { + return nil + } + return resp.Payload.Data[0] +} + +func EdgeRouterFromFilter(client *rest_management_api_client.ZitiEdgeManagement, filter string) *rest_model.EdgeRouterDetail { + params := &edge_router.ListEdgeRoutersParams{ + Filter: &filter, + } + params.SetTimeout(DefaultTimeout) + resp, err := client.EdgeRouter.ListEdgeRouters(params, nil) + if err != nil { + log.Errorf("Could not obtain an ID for the edge routers with filter %s: %v", filter, err) + return nil + } + if resp == nil || resp.Payload == nil || resp.Payload.Data == nil || len(resp.Payload.Data) == 0 { + return nil + } + return resp.Payload.Data[0] +} + +func ServiceEdgeRouterPolicyFromFilter(client *rest_management_api_client.ZitiEdgeManagement, filter string) *rest_model.ServiceEdgeRouterPolicyDetail { + params := &service_edge_router_policy.ListServiceEdgeRouterPoliciesParams{ + Filter: &filter, + } + params.SetTimeout(DefaultTimeout) + resp, err := client.ServiceEdgeRouterPolicy.ListServiceEdgeRouterPolicies(params, nil) + if err != nil { + log.Errorf("Could not obtain an ID for the ServiceEdgeRouterPolicy routers with filter %s: %v", filter, err) return nil } if resp == nil || resp.Payload == nil || resp.Payload.Data == nil || len(resp.Payload.Data) == 0 { @@ -87,7 +251,7 @@ func ServicePolicyFromFilter(client *rest_management_api_client.ZitiEdgeManageme } func NameFilter(name string) string { - return `name="` + name + `"` + return fmt.Sprintf("name = \"%s\"", name) } func NewClient() (*rest_management_api_client.ZitiEdgeManagement, error) { @@ -100,7 +264,7 @@ func NewClient() (*rest_management_api_client.ZitiEdgeManagement, error) { if cachedId == nil { return nil, errors.New("no identity found") } - + caPool := x509.NewCertPool() if _, cacertErr := os.Stat(cachedId.CaCert); cacertErr == nil { rootPemData, err := os.ReadFile(cachedId.CaCert) @@ -111,7 +275,7 @@ func NewClient() (*rest_management_api_client.ZitiEdgeManagement, error) { } else { return nil, errors.New("CA cert file not found in config file") } - + tlsConfig := &tls.Config{ RootCAs: caPool, } @@ -137,4 +301,4 @@ func NewClient() (*rest_management_api_client.ZitiEdgeManagement, error) { return nil, errors.New("client not authenticated. login with 'ziti edge login' before executing") } return c, nil -} \ No newline at end of file +} diff --git a/ziti/cmd/ascode/ascode_test.go b/ziti/cmd/ascode/ascode_test.go new file mode 100644 index 000000000..9bd9fbaa4 --- /dev/null +++ b/ziti/cmd/ascode/ascode_test.go @@ -0,0 +1,238 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 cmd + +import ( + "context" + "crypto/tls" + "fmt" + "github.com/antchfx/jsonquery" + "github.com/michaelquigley/pfxlog" + "github.com/openziti/ziti/ziti/cmd/ascode/exporter" + "github.com/openziti/ziti/ziti/cmd/ascode/importer" + "github.com/openziti/ziti/ziti/cmd/edge" + "github.com/stretchr/testify/assert" + "net/http" + "os" + "strings" + "testing" + "time" +) + +var log = pfxlog.Logger() + +func TestYamlUploadAndDownload(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cmdComplete := make(chan bool) + qsCmd := edge.NewQuickStartCmd(os.Stdout, os.Stderr, ctx) + + tmp, _ := os.MkdirTemp("/tmp", "ziti-") + qsCmd.SetArgs([]string{"--home", tmp, "--ctrl-address", "127.0.0.1"}) + os.Setenv("ZITI_CONFIG_DIR", tmp) + os.Setenv("ZITI_HOME", tmp) + + go func() { + err := qsCmd.Execute() + if err != nil { + log.Fatal(err) + } + cmdComplete <- true + }() + + c := make(chan struct{}) + go waitForController("https://127.0.0.1:1280", c) + timeout, _ := time.ParseDuration("180000000s") + select { + case <-c: + //completed normally + log.Info("controller online") + case <-time.After(timeout): + cancel() + panic("timed out waiting for controller") + } + + login := edge.NewLoginCmd(os.Stdout, os.Stderr) + login.SetArgs([]string{"127.0.0.1:1280", "-y", "--username", "admin", "--password", "admin"}) + err := login.Execute() + if err != nil { + log.Fatal(err) + } + + performTest(t) + + cancel() //terminate the running ctrl/router + + <-cmdComplete + fmt.Println("Operation completed") +} + +func waitForController(ctrlUrl string, done chan struct{}) { + tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} + client := &http.Client{Transport: tr} + for { + r, e := client.Get(ctrlUrl) + if e != nil || r == nil || r.StatusCode != 200 { + time.Sleep(50 * time.Millisecond) + } else { + break + } + } + done <- struct{}{} + +} + +func performTest(t *testing.T) { + + errWriter := strings.Builder{} + + uploadWriter := strings.Builder{} + uploadCmd := importer.NewImportCmd(&uploadWriter, &errWriter) + uploadCmd.SetArgs([]string{"--verbose", "--yaml", "./test.yaml"}) + + err := uploadCmd.Execute() + if err != nil { + t.Fail() + log.Fatal(err) + } + + downloadWriter := strings.Builder{} + downloadCmd := exporter.NewExportCmd(&downloadWriter, &errWriter) + downloadCmd.SetArgs([]string{"--json"}) + + err = downloadCmd.Execute() + if err != nil { + t.Fail() + log.Fatal(err) + } + + result := downloadWriter.String() + + log.Debug("Read " + result) + + doc, err := jsonquery.Parse(strings.NewReader(result)) + if err != nil { + panic(err) + } + + assert.NotEqual(t, 0, len(doc.ChildNodes())) + if len(doc.ChildNodes()) == 0 { + // no data, nothing to test + return + } + + externalJwtSigner1 := jsonquery.FindOne(doc, "//externalJwtSigners/*[name='NetFoundry Console Integration External JWT Signer']") + assert.Equal(t, "https://gateway.staging.netfoundry.io/network-auth/v1/public/.well-known/NYFw7IGJKNP9AaG45iwCj/jwks.json", jsonquery.FindOne(externalJwtSigner1, "/jwksEndpoint").Value()) + assert.Equal(t, "https://gateway.staging.netfoundry.io/cloudziti/25ba1aa3-4468-445a-910e-93f5b425f2c1", jsonquery.FindOne(externalJwtSigner1, "/audience").Value()) + + authPolicy1 := jsonquery.FindOne(doc, "//authPolicies/*[name='NetFoundry Console Integration Auth Policy']") + assert.Equal(t, "@NetFoundry Console Integration External JWT Signer", jsonquery.FindOne(authPolicy1, "/primary/extJwt/allowedSigners/*[1]").Value()) + authPolicy2 := jsonquery.FindOne(doc, "//authPolicies/*[name='Test123']") + assert.True(t, jsonquery.FindOne(authPolicy2, "/primary/updb/allowed").Value().(bool)) + + identity1 := jsonquery.FindOne(doc, "//identities/*[externalId='f1505b76-38ec-470b-9819-75984623c23d']") + assert.Equal(t, "Vinay Lakshmaiah", jsonquery.FindOne(identity1, "/name").Value()) + assert.Empty(t, jsonquery.FindOne(identity1, "/roleAttributes").Value()) + assert.Equal(t, "@NetFoundry Console Integration Auth Policy", jsonquery.FindOne(identity1, "/authPolicy").Value()) + + config1 := jsonquery.FindOne(doc, "//configs/*[name='service2-intercept-config']") + assert.Equal(t, "@intercept.v1", jsonquery.FindOne(config1, "/configType").Value()) + config2 := jsonquery.FindOne(doc, "//configs/*[name='ssssimple-intercept-config']") + assert.Equal(t, "@intercept.v1", jsonquery.FindOne(config2, "/configType").Value()) + config3 := jsonquery.FindOne(doc, "//configs/*[name='test-123-host-config']") + assert.Equal(t, "@host.v1", jsonquery.FindOne(config3, "/configType").Value()) + config4 := jsonquery.FindOne(doc, "//configs/*[name='service1-host-config']") + assert.Equal(t, "@host.v1", jsonquery.FindOne(config4, "/configType").Value()) + + postureCheck1 := jsonquery.FindOne(doc, "//postureChecks/*[name='Mac']") + assert.Equal(t, "MAC", jsonquery.FindOne(postureCheck1, "/typeId").Value()) + assert.Equal(t, "0123456789ab", jsonquery.FindOne(postureCheck1, "/macAddresses/*[1]").Value()) + assert.Equal(t, "mac", jsonquery.FindOne(postureCheck1, "/roleAttributes/*[1]").Value()) + + postureCheck2 := jsonquery.FindOne(doc, "//postureChecks/*[name='MFA']") + assert.Equal(t, "MFA", jsonquery.FindOne(postureCheck2, "/typeId").Value()) + assert.Equal(t, "mfa", jsonquery.FindOne(postureCheck2, "/roleAttributes/*[1]").Value()) + + posturecheck3 := jsonquery.FindOne(doc, "//postureChecks/*[name='Process']") + assert.Equal(t, "PROCESS", jsonquery.FindOne(posturecheck3, "/typeId").Value()) + assert.Equal(t, "Linux", jsonquery.FindOne(posturecheck3, "/process/osType").Value()) + assert.Equal(t, "/path/something", jsonquery.FindOne(posturecheck3, "/process/path").Value()) + assert.Empty(t, jsonquery.Find(posturecheck3, "/process/hashes/*[1]")) + assert.Equal(t, "process", jsonquery.FindOne(posturecheck3, "/roleAttributes/*[1]").Value()) + + router1 := jsonquery.FindOne(doc, "//routers/*[name='custroutet2']") + assert.Equal(t, "vis-bind", jsonquery.FindOne(router1, "/roleAttributes/*[1]").Value()) + + router2 := jsonquery.FindOne(doc, "//routers/*[name='asd']") + assert.Empty(t, jsonquery.Find(router2, "/roleAttributes/*[1]")) + + router3 := jsonquery.FindOne(doc, "//routers/*[name='public-router1']") + assert.Equal(t, "public", jsonquery.FindOne(router3, "/roleAttributes/*[1]").Value()) + + router4 := jsonquery.FindOne(doc, "//routers/*[name='enroll']") + assert.Empty(t, jsonquery.FindOne(router4, "/roleAttributes").Value()) + + router5 := jsonquery.FindOne(doc, "//routers/*[name='nfhosted']") + assert.Equal(t, "public", jsonquery.FindOne(router5, "/roleAttributes/*[1]").Value()) + + router6 := jsonquery.FindOne(doc, "//routers/*[name='appdata']") + assert.Empty(t, jsonquery.FindOne(router6, "/roleAttributes").Value()) + assert.Equal(t, "er", jsonquery.FindOne(router6, "/appData/my").Value()) + + router7 := jsonquery.FindOne(doc, "//routers/*[name='vis-customer-router']") + assert.Equal(t, "vis-bind", jsonquery.FindOne(router7, "/roleAttributes/*").Value()) + + serviceRouterPolicy1 := jsonquery.FindOne(doc, "//serviceEdgeRouterPolicies/*[name='ssep2']") + assert.Equal(t, "@custroutet2", jsonquery.FindOne(serviceRouterPolicy1, "/edgeRouterRoles/*[1]").Value()) + assert.Equal(t, "@ssssimple", jsonquery.FindOne(serviceRouterPolicy1, "/serviceRoles/*[1]").Value()) + + serviceRouterPolicy2 := jsonquery.FindOne(doc, "//serviceEdgeRouterPolicies/*[name='sep1']") + assert.Equal(t, "@public-router1", jsonquery.FindOne(serviceRouterPolicy2, "/edgeRouterRoles/*[1]").Value()) + assert.Equal(t, "@ssssimple", jsonquery.FindOne(serviceRouterPolicy2, "/serviceRoles/*[1]").Value()) + + servicePolicy1 := jsonquery.FindOne(doc, "//servicePolicies/*[name='ssssimple-bind-policy']") + assert.Equal(t, "@public-router1", jsonquery.FindOne(servicePolicy1, "/identityRoles/*[1]").Value()) + assert.Equal(t, "@ssssimple", jsonquery.FindOne(servicePolicy1, "/serviceRoles/*[1]").Value()) + assert.Equal(t, "Bind", jsonquery.FindOne(servicePolicy1, "/type").Value()) + + servicePolicy2 := jsonquery.FindOne(doc, "//servicePolicies/*[name='ssssimple-dial-policy']") + assert.Equal(t, "@identity12", jsonquery.FindOne(servicePolicy2, "/identityRoles/*[1]").Value()) + assert.Equal(t, "@ssssimple", jsonquery.FindOne(servicePolicy2, "/serviceRoles/*[1]").Value()) + assert.Equal(t, "Dial", jsonquery.FindOne(servicePolicy2, "/type").Value()) + + routerPolicy1 := jsonquery.FindOne(doc, "//edgeRouterPolicies/*[name='routerpolicy1']") + assert.Equal(t, "@public-router1", jsonquery.FindOne(routerPolicy1, "/edgeRouterRoles/*[1]").Value()) + assert.Equal(t, "@identity12", jsonquery.FindOne(routerPolicy1, "/identityRoles/*[1]").Value()) + + routerPolicy2 := jsonquery.FindOne(doc, "//edgeRouterPolicies/*[name='edge-router-D98X8WmjYH-system']") + assert.Equal(t, "@custroutet2", jsonquery.FindOne(routerPolicy2, "/edgeRouterRoles/*[1]").Value()) + assert.Equal(t, "@custroutet2", jsonquery.FindOne(routerPolicy2, "/identityRoles/*[1]").Value()) + + service1 := jsonquery.FindOne(doc, "//services/*[name='ssssimple']") + assert.Equal(t, "abcd", jsonquery.FindOne(service1, "/roleAttributes/*[1]").Value()) + assert.Equal(t, "service", jsonquery.FindOne(service1, "/roleAttributes/*[2]").Value()) + configs1 := []string{} + for _, node := range jsonquery.Find(service1, "/configs/*") { + configs1 = append(configs1, node.Value().(string)) + } + assert.Contains(t, configs1, "@ssssimple-intercept-config") + assert.Contains(t, configs1, "@ssssimple-host-config") + + service2 := jsonquery.FindOne(doc, "//services/*[name='asdfasdf']") + assert.Equal(t, "bcde", jsonquery.FindOne(service2, "/roleAttributes/*[1]").Value()) + assert.Empty(t, jsonquery.Find(service2, "/configs/*")) + +} diff --git a/ziti/cmd/ascode/exporter/exporter.go b/ziti/cmd/ascode/exporter/exporter.go new file mode 100644 index 000000000..8ac66c229 --- /dev/null +++ b/ziti/cmd/ascode/exporter/exporter.go @@ -0,0 +1,342 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 exporter + +import ( + "encoding/json" + "errors" + "github.com/judedaryl/go-arrayutils" + "github.com/michaelquigley/pfxlog" + "github.com/openziti/edge-api/rest_management_api_client" + "github.com/openziti/ziti/internal" + "github.com/openziti/ziti/internal/rest/mgmt" + "github.com/openziti/ziti/ziti/cmd/edge" + "github.com/openziti/ziti/ziti/constants" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "io" + "os" + "slices" + "strings" +) + +var log = pfxlog.Logger() + +type Exporter struct { + loginOpts edge.LoginOptions + client *rest_management_api_client.ZitiEdgeManagement + ofJson bool + ofYaml bool + file *os.File + filename string + configCache map[string]any + configTypeCache map[string]any + authPolicyCache map[string]any + externalJwtCache map[string]any +} + +var output Output + +func NewExportCmd(out io.Writer, errOut io.Writer) *cobra.Command { + + exporter := &Exporter{} + exporter.loginOpts = edge.LoginOptions{} + + cmd := &cobra.Command{ + Use: "export [entity]", + Short: "Export entities", + Long: "Export all or selected entities.\n" + + "Valid entities are: [all|ca/certificate-authority|identity|edge-router|service|config|config-type|service-policy|edge-router-policy|service-edge-router-policy|external-jwt-signer|auth-policy|posture-check] (default all)", + Args: cobra.MinimumNArgs(0), + PersistentPreRun: func(cmd *cobra.Command, args []string) { + err := exporter.Init(out) + if err != nil { + panic(err) + } + }, + Run: func(cmd *cobra.Command, args []string) { + err := exporter.Execute(args) + if err != nil { + panic(err) + } + }, + Hidden: true, + } + + v := viper.New() + + // When we bind flags to environment variables expect that the + // environment variables are prefixed, d.g. a flag like --number + // binds to an environment variable STING_NUMBER. This helps + // avoid conflicts. + viper.SetEnvPrefix(constants.ZITI) // All env vars we seek will be prefixed with "ZITI_" + + // Environment variables can't have dashes in them, so bind them to their equivalent + // keys with underscores, d.g. --favorite-color to STING_FAVORITE_COLOR + v.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + + v.AutomaticEnv() + + cmd.Flags().SetInterspersed(true) + cmd.Flags().BoolVar(&exporter.ofJson, "json", true, "Output in JSON") + cmd.Flags().BoolVar(&exporter.ofYaml, "yaml", false, "Output in YAML") + cmd.MarkFlagsMutuallyExclusive("json", "yaml") + + cmd.Flags().StringVarP(&exporter.filename, "output-file", "o", "", "Write output to local file") + + edge.AddLoginFlags(cmd, &exporter.loginOpts) + exporter.loginOpts.Out = out + exporter.loginOpts.Err = errOut + + return cmd +} + +func (exporter *Exporter) Init(out io.Writer) error { + + logLvl := logrus.InfoLevel + if exporter.loginOpts.Verbose { + logLvl = logrus.DebugLevel + } + + pfxlog.GlobalInit(logLvl, pfxlog.DefaultOptions().Color()) + internal.ConfigureLogFormat(logLvl) + + client, err := mgmt.NewClient() + if err != nil { + loginErr := exporter.loginOpts.Run() + if loginErr != nil { + log.Fatal(err) + } + client, err = mgmt.NewClient() + if err != nil { + log.Fatal(err) + } + } + exporter.client = client + + if exporter.filename != "" { + o, err := NewOutputToFile(exporter.loginOpts.Verbose, exporter.ofJson, exporter.ofYaml, exporter.filename, exporter.loginOpts.Err) + if err != nil { + return err + } + output = *o + } else { + o, err := NewOutputToWriter(exporter.loginOpts.Verbose, exporter.ofJson, exporter.ofYaml, out, exporter.loginOpts.Err) + if err != nil { + return err + } + output = *o + } + + return nil +} + +func (exporter *Exporter) Execute(input []string) error { + + args := arrayutils.Map(input, strings.ToLower) + + exporter.authPolicyCache = map[string]any{} + exporter.configCache = map[string]any{} + exporter.configTypeCache = map[string]any{} + exporter.externalJwtCache = map[string]any{} + + result := map[string]interface{}{} + + if exporter.IsCertificateAuthorityExportRequired(args) { + log.Debug("Processing Certificate Authorities") + cas, err := exporter.GetCertificateAuthorities() + if err != nil { + return err + } + result["certificateAuthorities"] = cas + } + if exporter.IsIdentityExportRequired(args) { + log.Debug("Processing Identities") + identities, err := exporter.GetIdentities() + if err != nil { + return err + } + result["identities"] = identities + } + + if exporter.IsEdgeRouterExportRequired(args) { + log.Debug("Processing Edge Routers") + routers, err := exporter.GetEdgeRouters() + if err != nil { + return err + } + result["edgeRouters"] = routers + } + if exporter.IsServiceExportRequired(args) { + log.Debug("Processing Services") + services, err := exporter.GetServices() + if err != nil { + return err + } + result["services"] = services + } + if exporter.IsConfigExportRequired(args) { + log.Debug("Processing Configs") + configs, err := exporter.GetConfigs() + if err != nil { + return err + } + result["configs"] = configs + } + if exporter.IsConfigTypeExportRequired(args) { + log.Debug("Processing Config Types") + configTypes, err := exporter.GetConfigTypes() + if err != nil { + return err + } + result["configTypes"] = configTypes + } + if exporter.IsServicePolicyExportRequired(args) { + log.Debug("Processing Service Policies") + servicePolicies, err := exporter.GetServicePolicies() + if err != nil { + return err + } + result["servicePolicies"] = servicePolicies + } + if exporter.IsEdgeRouterExportRequired(args) { + log.Debug("Processing Edge Router Policies") + routerPolicies, err := exporter.GetEdgeRouterPolicies() + if err != nil { + return err + } + result["edgeRouterPolicies"] = routerPolicies + } + if exporter.IsServiceEdgeRouterPolicyExportRequired(args) { + log.Debug("Processing Service Edge Router Policies") + serviceRouterPolicies, err := exporter.GetServiceEdgeRouterPolicies() + if err != nil { + return err + } + result["serviceEdgeRouterPolicies"] = serviceRouterPolicies + } + if exporter.IsExtJwtSignerExportRequired(args) { + log.Debug("Processing External JWT Signers") + externalJwtSigners, err := exporter.GetExternalJwtSigners() + if err != nil { + return err + } + result["externalJwtSigners"] = externalJwtSigners + } + if exporter.IsAuthPolicyExportRequired(args) { + log.Debug("Processing Auth Policies") + authPolicies, err := exporter.GetAuthPolicies() + if err != nil { + return err + } + result["authPolicies"] = authPolicies + } + if exporter.IsPostureCheckExportRequired(args) { + log.Debug("Processing Posture Checks") + postureChecks, err := exporter.GetPostureChecks() + if err != nil { + return err + } + result["postureChecks"] = postureChecks + } + + log.Debug("Export complete") + + err := output.Write(result) + if err != nil { + return err + } + if exporter.file != nil { + err := exporter.file.Close() + if err != nil { + return err + } + } + + return nil +} + +type ClientCount func() (int64, error) +type ClientList func(offset *int64, limit *int64) ([]interface{}, error) +type EntityProcessor func(item interface{}) (map[string]interface{}, error) + +func (exporter *Exporter) getEntities(entityName string, count ClientCount, list ClientList, processor EntityProcessor) ([]map[string]interface{}, error) { + + totalCount, countErr := count() + if countErr != nil { + return nil, errors.Join(errors.New("error reading total number of "+entityName), countErr) + } + + result := []map[string]interface{}{} + + offset := int64(0) + limit := int64(500) + more := true + for more { + resp, err := list(&offset, &limit) + _, _ = internal.FPrintfReusingLine(exporter.loginOpts.Err, "Reading %d/%d %s", offset, totalCount, entityName) + if err != nil { + return nil, errors.Join(errors.New("error reading "+entityName), err) + } + + for _, item := range resp { + m, err := processor(item) + if err != nil { + return nil, err + } + if m != nil { + result = append(result, m) + } + } + + more = offset < totalCount + offset += limit + } + + _, _ = internal.FPrintflnReusingLine(exporter.loginOpts.Err, "Read %d %s", len(result), entityName) + + return result, nil + +} + +func (exporter *Exporter) ToMap(input interface{}) map[string]interface{} { + jsonData, _ := json.MarshalIndent(input, "", "") + m := map[string]interface{}{} + err := json.Unmarshal(jsonData, &m) + if err != nil { + log.WithError(err).Error("error converting input to map") + return map[string]interface{}{} + } + return m +} + +func (exporter *Exporter) defaultRoleAttributes(m map[string]interface{}) { + if m["roleAttributes"] == nil { + m["roleAttributes"] = []string{} + } +} + +func (exporter *Exporter) Filter(m map[string]interface{}, properties []string) { + + // remove any properties that are not requested + for k := range m { + if slices.Contains(properties, k) { + delete(m, k) + } + } +} diff --git a/ziti/cmd/ascode/exporter/exporter_auth_policies.go b/ziti/cmd/ascode/exporter/exporter_auth_policies.go new file mode 100644 index 000000000..e7ac040bd --- /dev/null +++ b/ziti/cmd/ascode/exporter/exporter_auth_policies.go @@ -0,0 +1,116 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 exporter + +import ( + "github.com/openziti/edge-api/rest_management_api_client/auth_policy" + "github.com/openziti/edge-api/rest_management_api_client/external_jwt_signer" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/ziti/internal/ascode" + "slices" +) + +func (exporter Exporter) IsAuthPolicyExportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "auth-policy") +} + +func (exporter Exporter) GetAuthPolicies() ([]map[string]interface{}, error) { + + return exporter.getEntities( + "AuthPolicies", + func() (int64, error) { + limit := int64(1) + resp, err := exporter.client.AuthPolicy.ListAuthPolicies( + &auth_policy.ListAuthPoliciesParams{Limit: &limit}, nil) + if err != nil { + return -1, err + } + return *resp.GetPayload().Meta.Pagination.TotalCount, nil + + }, + func(offset *int64, limit *int64) ([]interface{}, error) { + resp, err := exporter.client.AuthPolicy.ListAuthPolicies( + &auth_policy.ListAuthPoliciesParams{Limit: limit, Offset: offset}, nil) + if err != nil { + return nil, err + } + entities := make([]interface{}, len(resp.GetPayload().Data)) + for i, c := range resp.GetPayload().Data { + entities[i] = interface{}(c) + } + return entities, nil + }, + func(entity interface{}) (map[string]interface{}, error) { + + item := entity.(*rest_model.AuthPolicyDetail) + + if *item.Name != "Default" { + // convert to a map of values + m := exporter.ToMap(item) + + // filter unwanted properties + exporter.Filter(m, []string{"id", "_links", "createdAt", "updatedAt"}) + + // deleting Primary so we can reconstruct it + delete(m, "primary") + primary := exporter.ToMap(item.Primary) + m["primary"] = primary + // deleting ExtJwt so we can reconstruct it + delete(primary, "extJwt") + extJwt := exporter.ToMap(item.Primary.ExtJWT) + primary["extJwt"] = extJwt + // deleting AllowedSigners because it needs to use a reference to the name instead of the ID + delete(extJwt, "allowedSigners") + signers := []string{} + for _, signer := range item.Primary.ExtJWT.AllowedSigners { + extJwtSigner, lookupErr := ascode.GetItemFromCache(exporter.externalJwtCache, signer, func(id string) (interface{}, error) { + return exporter.client.ExternalJWTSigner.DetailExternalJWTSigner( + &external_jwt_signer.DetailExternalJWTSignerParams{ID: id}, nil) + }) + if lookupErr != nil { + return nil, lookupErr + } + signers = append(signers, "@"+*extJwtSigner.(*external_jwt_signer.DetailExternalJWTSignerOK).Payload.Data.Name) + } + extJwt["allowedSigners"] = signers + + // if a secondary jwt signer is set, update it with a name reference instead of the id + if item.Secondary.RequireExtJWTSigner != nil { + // deleting Secondary so we can reconstruct it + delete(m, "secondary") + secondary := exporter.ToMap(item.Secondary) + m["secondary"] = secondary + + // deleting RequiredExtJwtSigner because it needs to use a reference to the name instead of the ID + delete(secondary, "requiredExtJwtSigner") + requiredExtJwtSigner := exporter.ToMap(item.Secondary.RequireExtJWTSigner) + extJwtSigner, lookupErr := ascode.GetItemFromCache(exporter.externalJwtCache, *item.Secondary.RequireExtJWTSigner, func(id string) (interface{}, error) { + return exporter.client.ExternalJWTSigner.DetailExternalJWTSigner(&external_jwt_signer.DetailExternalJWTSignerParams{ID: id}, nil) + }) + if lookupErr != nil { + return nil, lookupErr + } + requiredExtJwtSigner["requiredExtJwtSigner"] = "@" + *extJwtSigner.(*external_jwt_signer.DetailExternalJWTSignerOK).Payload.Data.Name + } + + return m, nil + } + return nil, nil + }) + +} diff --git a/ziti/cmd/ascode/exporter/exporter_certificate_authorities.go b/ziti/cmd/ascode/exporter/exporter_certificate_authorities.go new file mode 100644 index 000000000..d15645ea4 --- /dev/null +++ b/ziti/cmd/ascode/exporter/exporter_certificate_authorities.go @@ -0,0 +1,69 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 exporter + +import ( + "github.com/openziti/edge-api/rest_management_api_client/certificate_authority" + "github.com/openziti/edge-api/rest_model" + "slices" +) + +func (exporter Exporter) IsCertificateAuthorityExportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "ca") || + slices.Contains(args, "certificate-authority") +} + +func (exporter Exporter) GetCertificateAuthorities() ([]map[string]interface{}, error) { + + return exporter.getEntities( + "CertificateAuthorities", + + func() (int64, error) { + limit := int64(1) + resp, err := exporter.client.CertificateAuthority.ListCas(&certificate_authority.ListCasParams{Limit: &limit}, nil) + if err != nil { + return -1, err + } + return *resp.GetPayload().Meta.Pagination.TotalCount, nil + }, + + func(offset *int64, limit *int64) ([]interface{}, error) { + resp, err := exporter.client.CertificateAuthority.ListCas(&certificate_authority.ListCasParams{Offset: offset, Limit: limit}, nil) + if err != nil { + return nil, err + } + entities := make([]interface{}, len(resp.GetPayload().Data)) + for i, c := range resp.GetPayload().Data { + entities[i] = interface{}(c) + } + return entities, nil + }, + + func(entity interface{}) (map[string]interface{}, error) { + + item := entity.(*rest_model.CaDetail) + + // convert to a map of values + m := exporter.ToMap(item) + + // filter unwanted properties + exporter.Filter(m, []string{"id", "_links", "createdAt", "updatedAt"}) + + return m, nil + }) +} diff --git a/ziti/cmd/ascode/exporter/exporter_config_types.go b/ziti/cmd/ascode/exporter/exporter_config_types.go new file mode 100644 index 000000000..94e6bd8f9 --- /dev/null +++ b/ziti/cmd/ascode/exporter/exporter_config_types.go @@ -0,0 +1,69 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 exporter + +import ( + "github.com/openziti/edge-api/rest_management_api_client/config" + "github.com/openziti/edge-api/rest_model" + "slices" +) + +func (exporter Exporter) IsConfigTypeExportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "config-type") +} + +func (exporter Exporter) GetConfigTypes() ([]map[string]interface{}, error) { + + return exporter.getEntities( + "ConfigTypes", + + func() (int64, error) { + limit := int64(1) + resp, err := exporter.client.Config.ListConfigTypes(&config.ListConfigTypesParams{Limit: &limit}, nil) + if err != nil { + return -1, err + } + return *resp.GetPayload().Meta.Pagination.TotalCount, nil + }, + + func(offset *int64, limit *int64) ([]interface{}, error) { + resp, _ := exporter.client.Config.ListConfigTypes(&config.ListConfigTypesParams{Limit: limit, Offset: offset}, nil) + entities := make([]interface{}, len(resp.GetPayload().Data)) + for i, c := range resp.GetPayload().Data { + entities[i] = interface{}(c) + } + return entities, nil + }, + + func(entity interface{}) (map[string]interface{}, error) { + + item := entity.(*rest_model.ConfigTypeDetail) + wellknownTypes := []string{"intercept.v1", "host.v1", "host.v2", "ziti-tunneler-server.v1", "ziti-tunneler-client.v1"} + + // don't include the wellknown types, they already exist when a network is created + if slices.Contains(wellknownTypes, *item.Name) { + return nil, nil + } + + // convert to a map of values + m := exporter.ToMap(item) + exporter.Filter(m, []string{"id", "_links", "createdAt", "updatedAt"}) + + return m, nil + }) +} diff --git a/ziti/cmd/ascode/exporter/exporter_configs.go b/ziti/cmd/ascode/exporter/exporter_configs.go new file mode 100644 index 000000000..ed2dfe0e1 --- /dev/null +++ b/ziti/cmd/ascode/exporter/exporter_configs.go @@ -0,0 +1,78 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 exporter + +import ( + "errors" + "github.com/openziti/edge-api/rest_management_api_client/config" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/ziti/internal/ascode" + "slices" +) + +func (exporter Exporter) IsConfigExportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "config") +} + +func (exporter Exporter) GetConfigs() ([]map[string]interface{}, error) { + + return exporter.getEntities( + "Configs", + + func() (int64, error) { + limit := int64(1) + resp, err := exporter.client.Config.ListConfigs(&config.ListConfigsParams{Limit: &limit}, nil) + if err != nil { + return -1, err + } + return *resp.GetPayload().Meta.Pagination.TotalCount, nil + }, + + func(offset *int64, limit *int64) ([]interface{}, error) { + resp, _ := exporter.client.Config.ListConfigs(&config.ListConfigsParams{Limit: limit, Offset: offset}, nil) + entities := make([]interface{}, len(resp.GetPayload().Data)) + for i, c := range resp.GetPayload().Data { + entities[i] = interface{}(c) + } + return entities, nil + }, + + func(entity interface{}) (map[string]interface{}, error) { + + item := entity.(*rest_model.ConfigDetail) + + // convert to a map of values + m := exporter.ToMap(item) + + // filter unwanted properties + exporter.Filter(m, []string{"id", "_links", "createdAt", "updatedAt"}) + + // translate ids to names + delete(m, "configType") + delete(m, "configTypeId") + configType, lookupErr := ascode.GetItemFromCache(exporter.configTypeCache, *item.ConfigTypeID, func(id string) (interface{}, error) { + return exporter.client.Config.DetailConfigType(&config.DetailConfigTypeParams{ID: id}, nil) + }) + if lookupErr != nil { + return nil, errors.Join(errors.New("error reading Auth Policy: "+*item.ConfigTypeID), lookupErr) + } + m["configType"] = "@" + *configType.(*config.DetailConfigTypeOK).Payload.Data.Name + + return m, nil + }) +} diff --git a/ziti/cmd/ascode/exporter/exporter_edgerouter_policies.go b/ziti/cmd/ascode/exporter/exporter_edgerouter_policies.go new file mode 100644 index 000000000..4087082b3 --- /dev/null +++ b/ziti/cmd/ascode/exporter/exporter_edgerouter_policies.go @@ -0,0 +1,81 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 exporter + +import ( + "github.com/openziti/edge-api/rest_management_api_client/edge_router_policy" + "github.com/openziti/edge-api/rest_model" + "slices" +) + +func (exporter Exporter) IsEdgeRouterPolicyExportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "edge-router-policy") +} + +func (exporter Exporter) GetEdgeRouterPolicies() ([]map[string]interface{}, error) { + + return exporter.getEntities( + "EdgeRouterPolicies", + + func() (int64, error) { + limit := int64(1) + resp, err := exporter.client.EdgeRouterPolicy.ListEdgeRouterPolicies(&edge_router_policy.ListEdgeRouterPoliciesParams{Limit: &limit}, nil) + if err != nil { + return -1, err + } + return *resp.GetPayload().Meta.Pagination.TotalCount, nil + }, + + func(offset *int64, limit *int64) ([]interface{}, error) { + resp, err := exporter.client.EdgeRouterPolicy.ListEdgeRouterPolicies(&edge_router_policy.ListEdgeRouterPoliciesParams{Limit: limit, Offset: offset}, nil) + if err != nil { + return nil, err + } + entities := make([]interface{}, len(resp.GetPayload().Data)) + for i, c := range resp.GetPayload().Data { + entities[i] = interface{}(c) + } + return entities, nil + }, + + func(entity interface{}) (map[string]interface{}, error) { + + item := entity.(*rest_model.EdgeRouterPolicyDetail) + + // convert to a map of values + m := exporter.ToMap(item) + + // translate attributes so they don't reference ids + identityRoles := []string{} + for _, role := range item.IdentityRolesDisplay { + identityRoles = append(identityRoles, role.Name) + } + m["identityRoles"] = identityRoles + edgeRouterRoles := []string{} + for _, role := range item.EdgeRouterRolesDisplay { + edgeRouterRoles = append(edgeRouterRoles, role.Name) + } + m["edgeRouterRoles"] = edgeRouterRoles + + // filter unwanted properties + exporter.Filter(m, []string{"id", "_links", "createdAt", "updatedAt", + "edgeRouterRolesDisplay", "identityRolesDisplay", "isSystem"}) + + return m, nil + }) +} diff --git a/ziti/cmd/ascode/exporter/exporter_edgerouters.go b/ziti/cmd/ascode/exporter/exporter_edgerouters.go new file mode 100644 index 000000000..386d80a37 --- /dev/null +++ b/ziti/cmd/ascode/exporter/exporter_edgerouters.go @@ -0,0 +1,70 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 exporter + +import ( + "github.com/openziti/edge-api/rest_management_api_client/edge_router" + "github.com/openziti/edge-api/rest_model" + "slices" +) + +func (exporter Exporter) IsEdgeRouterExportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "edge-router") || + slices.Contains(args, "er") +} + +func (exporter Exporter) GetEdgeRouters() ([]map[string]interface{}, error) { + + return exporter.getEntities( + "EdgeRouters", + func() (int64, error) { + limit := int64(1) + resp, err := exporter.client.EdgeRouter.ListEdgeRouters(&edge_router.ListEdgeRoutersParams{Limit: &limit}, nil) + if err != nil { + return -1, err + } + return *resp.GetPayload().Meta.Pagination.TotalCount, nil + }, + + func(offset *int64, limit *int64) ([]interface{}, error) { + resp, err := exporter.client.EdgeRouter.ListEdgeRouters(&edge_router.ListEdgeRoutersParams{Limit: limit, Offset: offset}, nil) + if err != nil { + return nil, err + } + entities := make([]interface{}, len(resp.GetPayload().Data)) + for i, c := range resp.GetPayload().Data { + entities[i] = interface{}(c) + } + return entities, nil + }, + + func(entity interface{}) (map[string]interface{}, error) { + + item := entity.(*rest_model.EdgeRouterDetail) + + // convert to a map of values + m := exporter.ToMap(item) + exporter.defaultRoleAttributes(m) + + // filter unwanted properties + exporter.Filter(m, []string{"id", "_links", "createdAt", "updatedAt", + "cost", "fingerprint", "isVerified", "isOnline", "enrollmentJwt", "enrollmentCreatedAt", "enrollmentExpiresAt", "syncStatus", "versionInfo", "certPem", "supportedProtocols"}) + + return m, nil + }) +} diff --git a/ziti/cmd/ascode/exporter/exporter_external_jwt_signers.go b/ziti/cmd/ascode/exporter/exporter_external_jwt_signers.go new file mode 100644 index 000000000..3163dcfa2 --- /dev/null +++ b/ziti/cmd/ascode/exporter/exporter_external_jwt_signers.go @@ -0,0 +1,71 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 exporter + +import ( + "github.com/openziti/edge-api/rest_management_api_client/external_jwt_signer" + "github.com/openziti/edge-api/rest_model" + "slices" +) + +func (exporter Exporter) IsExtJwtSignerExportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "ext-jwt-signer") || + slices.Contains(args, "external-jwt-signer") +} + +func (exporter Exporter) GetExternalJwtSigners() ([]map[string]interface{}, error) { + + return exporter.getEntities( + "ExtJWTSigners", + + func() (int64, error) { + limit := int64(1) + resp, err := exporter.client.ExternalJWTSigner.ListExternalJWTSigners(&external_jwt_signer.ListExternalJWTSignersParams{Limit: &limit}, nil) + if err != nil { + return -1, err + } + return *resp.GetPayload().Meta.Pagination.TotalCount, nil + }, + + func(offset *int64, limit *int64) ([]interface{}, error) { + resp, err := exporter.client.ExternalJWTSigner.ListExternalJWTSigners( + &external_jwt_signer.ListExternalJWTSignersParams{Offset: offset, Limit: limit}, nil) + if err != nil { + return nil, err + } + entities := make([]interface{}, len(resp.GetPayload().Data)) + for i, c := range resp.GetPayload().Data { + entities[i] = interface{}(c) + } + return entities, nil + }, + + func(entity interface{}) (map[string]interface{}, error) { + + item := entity.(*rest_model.ExternalJWTSignerDetail) + + // convert to a map of values + m := exporter.ToMap(item) + + // filter unwanted properties + exporter.Filter(m, []string{"id", "_links", "createdAt", "updatedAt", + "notBefore", "notAfter", "commonName"}) + + return m, nil + }) +} diff --git a/ziti/cmd/ascode/exporter/exporter_identities.go b/ziti/cmd/ascode/exporter/exporter_identities.go new file mode 100644 index 000000000..3de50051f --- /dev/null +++ b/ziti/cmd/ascode/exporter/exporter_identities.go @@ -0,0 +1,94 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 exporter + +import ( + "errors" + "github.com/openziti/edge-api/rest_management_api_client/auth_policy" + "github.com/openziti/edge-api/rest_management_api_client/identity" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/ziti/internal/ascode" + "slices" +) + +func (exporter Exporter) IsIdentityExportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "identity") +} + +func (exporter Exporter) GetIdentities() ([]map[string]interface{}, error) { + + return exporter.getEntities( + "Identities", + + func() (int64, error) { + limit := int64(1) + resp, err := exporter.client.Identity.ListIdentities(&identity.ListIdentitiesParams{Limit: &limit}, nil) + if err != nil { + return -1, err + } + return *resp.GetPayload().Meta.Pagination.TotalCount, nil + }, + + func(offset *int64, limit *int64) ([]interface{}, error) { + resp, err := exporter.client.Identity.ListIdentities(&identity.ListIdentitiesParams{Offset: offset, Limit: limit}, nil) + if err != nil { + return nil, err + } + entities := make([]interface{}, len(resp.GetPayload().Data)) + for i, c := range resp.GetPayload().Data { + entities[i] = interface{}(c) + } + return entities, nil + }, + + func(entity interface{}) (map[string]interface{}, error) { + + item := entity.(*rest_model.IdentityDetail) + + // only exporter regular identities and not the default admin + if *item.TypeID != "Router" && !*item.IsDefaultAdmin { + + // convert to a map of values + m := exporter.ToMap(item) + exporter.defaultRoleAttributes(m) + + // filter unwanted properties + exporter.Filter(m, []string{"id", "_links", "createdAt", "updatedAt", + "defaultHostingCost", "defaultHostingPrecedence", "hasApiSession", "serviceHostingPrecedences", "enrollment", + "appData", "sdkInfo", "disabledAt", "disabledUntil", "serviceHostingCosts", "envInfo", "authenticators", "type", "authPolicyId", + "hasRouterConnection", "hasEdgeRouterConnection"}) + + if item.DisabledUntil != nil { + m["disabledUntil"] = item.DisabledUntil + } + + // translate ids to names + authPolicy, lookupErr := ascode.GetItemFromCache(exporter.authPolicyCache, *item.AuthPolicyID, func(id string) (interface{}, error) { + return exporter.client.AuthPolicy.DetailAuthPolicy(&auth_policy.DetailAuthPolicyParams{ID: id}, nil) + }) + if lookupErr != nil { + return nil, errors.Join(errors.New("error reading Auth Policy: "+*item.AuthPolicyID), lookupErr) + } + m["authPolicy"] = "@" + *authPolicy.(*auth_policy.DetailAuthPolicyOK).GetPayload().Data.Name + return m, nil + } + + return nil, nil + }) + +} diff --git a/ziti/cmd/ascode/exporter/exporter_posture_checks.go b/ziti/cmd/ascode/exporter/exporter_posture_checks.go new file mode 100644 index 000000000..ebf2f8c0f --- /dev/null +++ b/ziti/cmd/ascode/exporter/exporter_posture_checks.go @@ -0,0 +1,63 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 exporter + +import ( + "github.com/openziti/edge-api/rest_management_api_client/posture_checks" + "golang.org/x/exp/slices" +) + +func (exporter Exporter) IsPostureCheckExportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "posture-check") +} + +func (exporter Exporter) GetPostureChecks() ([]map[string]interface{}, error) { + + return exporter.getEntities( + "PostureChecks", + + func() (int64, error) { + limit := int64(1) + resp, err := exporter.client.PostureChecks.ListPostureChecks(&posture_checks.ListPostureChecksParams{Limit: &limit}, nil) + if err != nil { + return -1, err + } + return *resp.GetPayload().Meta.Pagination.TotalCount, nil + }, + + func(offset *int64, limit *int64) ([]interface{}, error) { + resp, _ := exporter.client.PostureChecks.ListPostureChecks(&posture_checks.ListPostureChecksParams{Limit: limit, Offset: offset}, nil) + entities := make([]interface{}, len(resp.GetPayload().Data())) + for i, c := range resp.GetPayload().Data() { + entities[i] = interface{}(c) + } + return entities, nil + }, + + func(entity interface{}) (map[string]interface{}, error) { + + // convert to a map of values + m := exporter.ToMap(entity) + + // filter unwanted properties + exporter.Filter(m, []string{"id", "_links", "createdAt", "updatedAt", + "version"}) + + return m, nil + }) +} diff --git a/ziti/cmd/ascode/exporter/exporter_service_edgerouter_policies.go b/ziti/cmd/ascode/exporter/exporter_service_edgerouter_policies.go new file mode 100644 index 000000000..a069a813a --- /dev/null +++ b/ziti/cmd/ascode/exporter/exporter_service_edgerouter_policies.go @@ -0,0 +1,81 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 exporter + +import ( + "github.com/openziti/edge-api/rest_management_api_client/service_edge_router_policy" + "github.com/openziti/edge-api/rest_model" + "slices" +) + +func (exporter Exporter) IsServiceEdgeRouterPolicyExportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "service-edge-router-policy") +} + +func (exporter Exporter) GetServiceEdgeRouterPolicies() ([]map[string]interface{}, error) { + + return exporter.getEntities( + "ServiceEdgeRouterPolicies", + + func() (int64, error) { + limit := int64(1) + resp, err := exporter.client.ServiceEdgeRouterPolicy.ListServiceEdgeRouterPolicies(&service_edge_router_policy.ListServiceEdgeRouterPoliciesParams{Limit: &limit}, nil) + if err != nil { + return -1, err + } + return *resp.GetPayload().Meta.Pagination.TotalCount, nil + }, + + func(offset *int64, limit *int64) ([]interface{}, error) { + resp, err := exporter.client.ServiceEdgeRouterPolicy.ListServiceEdgeRouterPolicies(&service_edge_router_policy.ListServiceEdgeRouterPoliciesParams{Limit: limit, Offset: offset}, nil) + if err != nil { + return nil, err + } + entities := make([]interface{}, len(resp.GetPayload().Data)) + for i, c := range resp.GetPayload().Data { + entities[i] = interface{}(c) + } + return entities, nil + }, + + func(entity interface{}) (map[string]interface{}, error) { + + item := entity.(*rest_model.ServiceEdgeRouterPolicyDetail) + + // convert to a map of values + m := exporter.ToMap(item) + + // translate attributes so they don't reference ids + serviceRoles := []string{} + for _, role := range item.ServiceRolesDisplay { + serviceRoles = append(serviceRoles, role.Name) + } + m["serviceRoles"] = serviceRoles + edgeRouterRoles := []string{} + for _, role := range item.EdgeRouterRolesDisplay { + edgeRouterRoles = append(edgeRouterRoles, role.Name) + } + m["edgeRouterRoles"] = edgeRouterRoles + + // filter unwanted properties + exporter.Filter(m, []string{"id", "_links", "createdAt", "updatedAt", + "edgeRouterRolesDisplay", "serviceRolesDisplay", "isSystem"}) + + return m, nil + }) +} diff --git a/ziti/cmd/ascode/exporter/exporter_service_policies.go b/ziti/cmd/ascode/exporter/exporter_service_policies.go new file mode 100644 index 000000000..5f39e5734 --- /dev/null +++ b/ziti/cmd/ascode/exporter/exporter_service_policies.go @@ -0,0 +1,87 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 exporter + +import ( + "github.com/openziti/edge-api/rest_management_api_client/service_policy" + "github.com/openziti/edge-api/rest_model" + "slices" +) + +func (exporter Exporter) IsServicePolicyExportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "service-policy") +} + +func (exporter Exporter) GetServicePolicies() ([]map[string]interface{}, error) { + + return exporter.getEntities( + "ServicePolicies", + + func() (int64, error) { + limit := int64(1) + resp, err := exporter.client.ServicePolicy.ListServicePolicies(&service_policy.ListServicePoliciesParams{Limit: &limit}, nil) + if err != nil { + return -1, err + } + return *resp.GetPayload().Meta.Pagination.TotalCount, nil + }, + + func(offset *int64, limit *int64) ([]interface{}, error) { + resp, err := exporter.client.ServicePolicy.ListServicePolicies(&service_policy.ListServicePoliciesParams{Limit: limit, Offset: offset}, nil) + if err != nil { + return nil, err + } + entities := make([]interface{}, len(resp.GetPayload().Data)) + for i, c := range resp.GetPayload().Data { + entities[i] = interface{}(c) + } + return entities, nil + }, + + func(entity interface{}) (map[string]interface{}, error) { + + item := entity.(*rest_model.ServicePolicyDetail) + + // convert to a map of values + m := exporter.ToMap(item) + + // translate attributes so they don't reference ids + identityRoles := []string{} + for _, role := range item.IdentityRolesDisplay { + identityRoles = append(identityRoles, role.Name) + } + m["identityRoles"] = identityRoles + serviceRoles := []string{} + for _, role := range item.ServiceRolesDisplay { + serviceRoles = append(serviceRoles, role.Name) + } + m["serviceRoles"] = serviceRoles + postureCheckRoles := []string{} + for _, role := range item.PostureCheckRolesDisplay { + identityRoles = append(identityRoles, role.Name) + } + m["postureCheckRoles"] = postureCheckRoles + + // filter unwanted properties + exporter.Filter(m, []string{"id", "_links", "createdAt", "updatedAt", + "serviceRolesDisplay", "identityRolesDisplay", "postureCheckRolesDisplay", "isSystem"}) + + return m, nil + }, + ) +} diff --git a/ziti/cmd/ascode/exporter/exporter_services.go b/ziti/cmd/ascode/exporter/exporter_services.go new file mode 100644 index 000000000..2ccdc3804 --- /dev/null +++ b/ziti/cmd/ascode/exporter/exporter_services.go @@ -0,0 +1,88 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 exporter + +import ( + "errors" + "github.com/openziti/edge-api/rest_management_api_client/config" + "github.com/openziti/edge-api/rest_management_api_client/service" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/ziti/internal/ascode" + "slices" +) + +func (exporter Exporter) IsServiceExportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "service") +} + +func (exporter Exporter) GetServices() ([]map[string]interface{}, error) { + + return exporter.getEntities( + "Services", + + func() (int64, error) { + limit := int64(1) + resp, err := exporter.client.Service.ListServices(&service.ListServicesParams{Limit: &limit}, nil) + if err != nil { + return -1, err + } + return *resp.GetPayload().Meta.Pagination.TotalCount, nil + }, + + func(offset *int64, limit *int64) ([]interface{}, error) { + resp, err := exporter.client.Service.ListServices(&service.ListServicesParams{Limit: limit, Offset: offset}, nil) + if err != nil { + return nil, err + } + entities := make([]interface{}, len(resp.GetPayload().Data)) + for i, c := range resp.GetPayload().Data { + entities[i] = interface{}(c) + } + return entities, nil + }, + + func(entity interface{}) (map[string]interface{}, error) { + + item := entity.(*rest_model.ServiceDetail) + + // convert to a map of values + m := exporter.ToMap(item) + + exporter.defaultRoleAttributes(m) + + // filter unwanted properties + exporter.Filter(m, []string{"id", "_links", "createdAt", "updatedAt", + "configs", "config", "data", "postureQueries", "permissions", "maxIdleTimeMillis"}) + + // translate ids to names + var configNames []string + for _, c := range item.Configs { + configDetail, lookupErr := ascode.GetItemFromCache(exporter.configCache, c, func(id string) (interface{}, error) { + return exporter.client.Config.DetailConfig(&config.DetailConfigParams{ID: id}, nil) + }) + if lookupErr != nil { + return nil, errors.Join(errors.New("error reading Config: "+c), lookupErr) + } + configNames = append(configNames, "@"+*configDetail.(*config.DetailConfigOK).Payload.Data.Name) + } + delete(m, "configs") + m["configs"] = configNames + + return m, nil + }) +} diff --git a/ziti/cmd/ascode/exporter/exporter_test.go b/ziti/cmd/ascode/exporter/exporter_test.go new file mode 100644 index 000000000..037066ed8 --- /dev/null +++ b/ziti/cmd/ascode/exporter/exporter_test.go @@ -0,0 +1,223 @@ +package exporter + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_InputArgs(t *testing.T) { + + exporter := Exporter{} + + t.Run("service export", func(t *testing.T) { + + assert.True(t, exporter.IsCertificateAuthorityExportRequired([]string{"certificate-authority"}), "should be exported") + assert.True(t, exporter.IsCertificateAuthorityExportRequired([]string{"all"}), "should be exported") + assert.True(t, exporter.IsCertificateAuthorityExportRequired([]string{}), "should be exported") + + assert.False(t, exporter.IsCertificateAuthorityExportRequired([]string{"service"}), "should not be exported") + assert.False(t, exporter.IsCertificateAuthorityExportRequired([]string{"config"}), "should not be exported") + assert.False(t, exporter.IsCertificateAuthorityExportRequired([]string{"config-type"}), "should not be exported") + assert.False(t, exporter.IsCertificateAuthorityExportRequired([]string{"identity"}), "should not be exported") + assert.False(t, exporter.IsCertificateAuthorityExportRequired([]string{"auth-policy"}), "should not be exported") + assert.False(t, exporter.IsCertificateAuthorityExportRequired([]string{"ext-jwt-signer"}), "should not be exported") + assert.False(t, exporter.IsCertificateAuthorityExportRequired([]string{"posture-check"}), "should not be exported") + assert.False(t, exporter.IsCertificateAuthorityExportRequired([]string{"service-policy"}), "should not be exported") + assert.False(t, exporter.IsCertificateAuthorityExportRequired([]string{"edge-router-policy"}), "should not be exported") + assert.False(t, exporter.IsCertificateAuthorityExportRequired([]string{"service-edge-router-policy"}), "should not be exported") + + }) + + t.Run("service export", func(t *testing.T) { + + assert.True(t, exporter.IsServiceExportRequired([]string{"service"}), "should be exported") + assert.True(t, exporter.IsServiceExportRequired([]string{"all"}), "should be exported") + assert.True(t, exporter.IsServiceExportRequired([]string{}), "should be exported") + + assert.False(t, exporter.IsServiceExportRequired([]string{"config"}), "should not be exported") + assert.False(t, exporter.IsServiceExportRequired([]string{"config-type"}), "should not be exported") + assert.False(t, exporter.IsServiceExportRequired([]string{"identity"}), "should not be exported") + assert.False(t, exporter.IsServiceExportRequired([]string{"auth-policy"}), "should not be exported") + assert.False(t, exporter.IsServiceExportRequired([]string{"ext-jwt-signer"}), "should not be exported") + assert.False(t, exporter.IsServiceExportRequired([]string{"posture-check"}), "should not be exported") + assert.False(t, exporter.IsServiceExportRequired([]string{"service-policy"}), "should not be exported") + assert.False(t, exporter.IsServiceExportRequired([]string{"edge-router-policy"}), "should not be exported") + assert.False(t, exporter.IsServiceExportRequired([]string{"service-edge-router-policy"}), "should not be exported") + + }) + + t.Run("config export", func(t *testing.T) { + + assert.True(t, exporter.IsConfigExportRequired([]string{"config"}), "should be exported") + assert.True(t, exporter.IsConfigExportRequired([]string{"all"}), "should be exported") + assert.True(t, exporter.IsConfigExportRequired([]string{}), "should be exported") + + assert.False(t, exporter.IsConfigExportRequired([]string{"service"}), "should not be exported") + assert.False(t, exporter.IsConfigExportRequired([]string{"config-type"}), "should not be exported") + assert.False(t, exporter.IsConfigExportRequired([]string{"identity"}), "should not be exported") + assert.False(t, exporter.IsConfigExportRequired([]string{"certificate-authority"}), "should not be exported") + assert.False(t, exporter.IsConfigExportRequired([]string{"auth-policy"}), "should not be exported") + assert.False(t, exporter.IsConfigExportRequired([]string{"ext-jwt-signer"}), "should not be exported") + assert.False(t, exporter.IsConfigExportRequired([]string{"posture-check"}), "should not be exported") + assert.False(t, exporter.IsConfigExportRequired([]string{"service-policy"}), "should not be exported") + assert.False(t, exporter.IsConfigExportRequired([]string{"edge-router-policy"}), "should not be exported") + assert.False(t, exporter.IsConfigExportRequired([]string{"service-edge-router-policy"}), "should not be exported") + + }) + + t.Run("config-type export", func(t *testing.T) { + + assert.True(t, exporter.IsConfigTypeExportRequired([]string{"config-type"}), "should be exported") + assert.True(t, exporter.IsConfigTypeExportRequired([]string{"all"}), "should be exported") + assert.True(t, exporter.IsConfigTypeExportRequired([]string{}), "should be exported") + + assert.False(t, exporter.IsConfigTypeExportRequired([]string{"service"}), "should not be exported") + assert.False(t, exporter.IsConfigTypeExportRequired([]string{"config"}), "should not be exported") + assert.False(t, exporter.IsConfigTypeExportRequired([]string{"certificate-authority"}), "should not be exported") + assert.False(t, exporter.IsConfigTypeExportRequired([]string{"identity"}), "should not be exported") + assert.False(t, exporter.IsConfigTypeExportRequired([]string{"auth-policy"}), "should not be exported") + assert.False(t, exporter.IsConfigTypeExportRequired([]string{"ext-jwt-signer"}), "should not be exported") + assert.False(t, exporter.IsConfigTypeExportRequired([]string{"posture-check"}), "should not be exported") + assert.False(t, exporter.IsConfigTypeExportRequired([]string{"service-policy"}), "should not be exported") + assert.False(t, exporter.IsConfigTypeExportRequired([]string{"edge-router-policy"}), "should not be exported") + assert.False(t, exporter.IsConfigTypeExportRequired([]string{"service-edge-router-policy"}), "should not be exported") + + }) + + t.Run("identity export", func(t *testing.T) { + + assert.True(t, exporter.IsIdentityExportRequired([]string{"identity"}), "should be exported") + assert.True(t, exporter.IsIdentityExportRequired([]string{"all"}), "should be exported") + assert.True(t, exporter.IsIdentityExportRequired([]string{}), "should be exported") + + assert.False(t, exporter.IsIdentityExportRequired([]string{"auth-policy"}), "should not be exported") + assert.False(t, exporter.IsIdentityExportRequired([]string{"ext-jwt-signer"}), "should not be exported") + assert.False(t, exporter.IsIdentityExportRequired([]string{"external-jwt-signer"}), "should not be exported") + assert.False(t, exporter.IsIdentityExportRequired([]string{"certificate-authority"}), "should not be exported") + assert.False(t, exporter.IsIdentityExportRequired([]string{"service"}), "should not be exported") + assert.False(t, exporter.IsIdentityExportRequired([]string{"config"}), "should not be exported") + assert.False(t, exporter.IsIdentityExportRequired([]string{"config-type"}), "should not be exported") + assert.False(t, exporter.IsIdentityExportRequired([]string{"posture-check"}), "should not be exported") + assert.False(t, exporter.IsIdentityExportRequired([]string{"service-policy"}), "should not be exported") + assert.False(t, exporter.IsIdentityExportRequired([]string{"edge-router-policy"}), "should not be exported") + assert.False(t, exporter.IsIdentityExportRequired([]string{"service-edge-router-policy"}), "should not be exported") + + }) + + t.Run("auth-policy export", func(t *testing.T) { + + assert.True(t, exporter.IsAuthPolicyExportRequired([]string{"auth-policy"}), "should be exported") + assert.True(t, exporter.IsAuthPolicyExportRequired([]string{"all"}), "should be exported") + assert.True(t, exporter.IsAuthPolicyExportRequired([]string{}), "should be exported") + + assert.False(t, exporter.IsAuthPolicyExportRequired([]string{"identity"}), "should not be exported") + assert.False(t, exporter.IsAuthPolicyExportRequired([]string{"ext-jwt-signer"}), "should not be exported") + assert.False(t, exporter.IsAuthPolicyExportRequired([]string{"external-jwt-signer"}), "should not be exported") + assert.False(t, exporter.IsAuthPolicyExportRequired([]string{"certificate-authority"}), "should not be exported") + assert.False(t, exporter.IsAuthPolicyExportRequired([]string{"service"}), "should not be exported") + assert.False(t, exporter.IsAuthPolicyExportRequired([]string{"config"}), "should not be exported") + assert.False(t, exporter.IsAuthPolicyExportRequired([]string{"config-type"}), "should not be exported") + assert.False(t, exporter.IsAuthPolicyExportRequired([]string{"posture-check"}), "should not be exported") + assert.False(t, exporter.IsAuthPolicyExportRequired([]string{"service-policy"}), "should not be exported") + assert.False(t, exporter.IsAuthPolicyExportRequired([]string{"edge-router-policy"}), "should not be exported") + assert.False(t, exporter.IsAuthPolicyExportRequired([]string{"service-edge-router-policy"}), "should not be exported") + + }) + + t.Run("ext-jwt-signer export", func(t *testing.T) { + + assert.True(t, exporter.IsExtJwtSignerExportRequired([]string{"ext-jwt-signer"}), "should be exported") + assert.True(t, exporter.IsExtJwtSignerExportRequired([]string{"external-jwt-signer"}), "should be exported") + assert.True(t, exporter.IsExtJwtSignerExportRequired([]string{"all"}), "should be exported") + assert.True(t, exporter.IsExtJwtSignerExportRequired([]string{}), "should be exported") + + assert.False(t, exporter.IsExtJwtSignerExportRequired([]string{"identity"}), "should not be exported") + assert.False(t, exporter.IsExtJwtSignerExportRequired([]string{"auth-policy"}), "should not be exported") + assert.False(t, exporter.IsExtJwtSignerExportRequired([]string{"certificate-authority"}), "should not be exported") + assert.False(t, exporter.IsExtJwtSignerExportRequired([]string{"service"}), "should not be exported") + assert.False(t, exporter.IsExtJwtSignerExportRequired([]string{"config"}), "should not be exported") + assert.False(t, exporter.IsExtJwtSignerExportRequired([]string{"config-type"}), "should not be exported") + assert.False(t, exporter.IsExtJwtSignerExportRequired([]string{"posture-check"}), "should not be exported") + assert.False(t, exporter.IsExtJwtSignerExportRequired([]string{"service-policy"}), "should not be exported") + assert.False(t, exporter.IsExtJwtSignerExportRequired([]string{"edge-router-policy"}), "should not be exported") + assert.False(t, exporter.IsExtJwtSignerExportRequired([]string{"service-edge-router-policy"}), "should not be exported") + + }) + + t.Run("posture-check export", func(t *testing.T) { + + assert.True(t, exporter.IsPostureCheckExportRequired([]string{"posture-check"}), "should be exported") + assert.True(t, exporter.IsPostureCheckExportRequired([]string{"all"}), "should be exported") + assert.True(t, exporter.IsPostureCheckExportRequired([]string{}), "should be exported") + + assert.False(t, exporter.IsPostureCheckExportRequired([]string{"service"}), "should not be exported") + assert.False(t, exporter.IsPostureCheckExportRequired([]string{"config"}), "should not be exported") + assert.False(t, exporter.IsPostureCheckExportRequired([]string{"config-type"}), "should not be exported") + assert.False(t, exporter.IsPostureCheckExportRequired([]string{"identity"}), "should not be exported") + assert.False(t, exporter.IsPostureCheckExportRequired([]string{"auth-policy"}), "should not be exported") + assert.False(t, exporter.IsPostureCheckExportRequired([]string{"ext-jwt-signer"}), "should not be exported") + assert.False(t, exporter.IsPostureCheckExportRequired([]string{"external-jwt-signer"}), "should not be exported") + assert.False(t, exporter.IsPostureCheckExportRequired([]string{"service-policy"}), "should not be exported") + assert.False(t, exporter.IsPostureCheckExportRequired([]string{"service-edge-router-policy"}), "should not be exported") + assert.False(t, exporter.IsPostureCheckExportRequired([]string{"edge-router-policy"}), "should not be exported") + + }) + + t.Run("service-policy export", func(t *testing.T) { + + assert.True(t, exporter.IsServicePolicyExportRequired([]string{"service-policy"}), "should be exported") + assert.True(t, exporter.IsServicePolicyExportRequired([]string{"all"}), "should be exported") + assert.True(t, exporter.IsServicePolicyExportRequired([]string{}), "should be exported") + + assert.False(t, exporter.IsServicePolicyExportRequired([]string{"service"}), "should not be exported") + assert.False(t, exporter.IsServicePolicyExportRequired([]string{"config"}), "should not be exported") + assert.False(t, exporter.IsServicePolicyExportRequired([]string{"config-type"}), "should not be exported") + assert.False(t, exporter.IsServicePolicyExportRequired([]string{"identity"}), "should not be exported") + assert.False(t, exporter.IsServicePolicyExportRequired([]string{"auth-policy"}), "should not be exported") + assert.False(t, exporter.IsServicePolicyExportRequired([]string{"ext-jwt-signer"}), "should not be exported") + assert.False(t, exporter.IsServicePolicyExportRequired([]string{"external-jwt-signer"}), "should not be exported") + assert.False(t, exporter.IsServicePolicyExportRequired([]string{"posture-check"}), "should not be exported") + assert.False(t, exporter.IsServicePolicyExportRequired([]string{"service-edge-router-policy"}), "should not be exported") + assert.False(t, exporter.IsServicePolicyExportRequired([]string{"edge-router-policy"}), "should not be exported") + + }) + + t.Run("service-edge-router-policy export", func(t *testing.T) { + + assert.True(t, exporter.IsServiceEdgeRouterPolicyExportRequired([]string{"service-edge-router-policy"}), "should be exported") + assert.True(t, exporter.IsServiceEdgeRouterPolicyExportRequired([]string{"all"}), "should be exported") + assert.True(t, exporter.IsServiceEdgeRouterPolicyExportRequired([]string{}), "should be exported") + + assert.False(t, exporter.IsServiceEdgeRouterPolicyExportRequired([]string{"service"}), "should not be exported") + assert.False(t, exporter.IsServiceEdgeRouterPolicyExportRequired([]string{"config"}), "should not be exported") + assert.False(t, exporter.IsServiceEdgeRouterPolicyExportRequired([]string{"config-type"}), "should not be exported") + assert.False(t, exporter.IsServiceEdgeRouterPolicyExportRequired([]string{"identity"}), "should not be exported") + assert.False(t, exporter.IsServiceEdgeRouterPolicyExportRequired([]string{"auth-policy"}), "should not be exported") + assert.False(t, exporter.IsServiceEdgeRouterPolicyExportRequired([]string{"ext-jwt-signer"}), "should not be exported") + assert.False(t, exporter.IsServiceEdgeRouterPolicyExportRequired([]string{"external-jwt-signer"}), "should not be exported") + assert.False(t, exporter.IsServiceEdgeRouterPolicyExportRequired([]string{"posture-check"}), "should not be exported") + assert.False(t, exporter.IsServiceEdgeRouterPolicyExportRequired([]string{"service-policy"}), "should not be exported") + assert.False(t, exporter.IsServiceEdgeRouterPolicyExportRequired([]string{"edge-router-policy"}), "should not be exported") + + }) + + t.Run("service-edge-router-policy export", func(t *testing.T) { + + assert.True(t, exporter.IsEdgeRouterPolicyExportRequired([]string{"edge-router-policy"}), "should be exported") + assert.True(t, exporter.IsEdgeRouterPolicyExportRequired([]string{"all"}), "should be exported") + assert.True(t, exporter.IsEdgeRouterPolicyExportRequired([]string{}), "should be exported") + + assert.False(t, exporter.IsEdgeRouterPolicyExportRequired([]string{"service"}), "should not be exported") + assert.False(t, exporter.IsEdgeRouterPolicyExportRequired([]string{"config"}), "should not be exported") + assert.False(t, exporter.IsEdgeRouterPolicyExportRequired([]string{"config-type"}), "should not be exported") + assert.False(t, exporter.IsEdgeRouterPolicyExportRequired([]string{"identity"}), "should not be exported") + assert.False(t, exporter.IsEdgeRouterPolicyExportRequired([]string{"auth-policy"}), "should not be exported") + assert.False(t, exporter.IsEdgeRouterPolicyExportRequired([]string{"ext-jwt-signer"}), "should not be exported") + assert.False(t, exporter.IsEdgeRouterPolicyExportRequired([]string{"external-jwt-signer"}), "should not be exported") + assert.False(t, exporter.IsEdgeRouterPolicyExportRequired([]string{"posture-check"}), "should not be exported") + assert.False(t, exporter.IsEdgeRouterPolicyExportRequired([]string{"service-policy"}), "should not be exported") + assert.False(t, exporter.IsEdgeRouterPolicyExportRequired([]string{"service-edge-router-policy"}), "should not be exported") + + }) + +} diff --git a/ziti/cmd/ascode/exporter/output.go b/ziti/cmd/ascode/exporter/output.go new file mode 100644 index 000000000..6ec2916eb --- /dev/null +++ b/ziti/cmd/ascode/exporter/output.go @@ -0,0 +1,142 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 exporter + +import ( + "bufio" + "encoding/json" + "github.com/openziti/ziti/internal" + "gopkg.in/yaml.v3" + + "io" + "os" +) + +type Output struct { + outputJson bool + outputYaml bool + filename string + writer *bufio.Writer + errWriter io.Writer + verbose bool +} + +func NewOutputToFile(verbose bool, outputJson bool, outputYaml bool, filename string, errWriter io.Writer) (*Output, error) { + file, err := os.Create(filename) + if err != nil { + log.WithError(err).Error("Error creating file for writing") + return nil, err + } + writer := bufio.NewWriter(file) + output, err := NewOutputToWriter(verbose, outputJson, outputYaml, writer, errWriter) + output.filename = filename + return output, err +} + +func NewOutputToWriter(verbose bool, outputJson bool, outputYaml bool, writer io.Writer, errWriter io.Writer) (*Output, error) { + output := Output{} + output.verbose = verbose + output.outputJson = outputJson + output.outputYaml = outputYaml + output.writer = bufio.NewWriter(writer) + output.errWriter = errWriter + return &output, nil +} + +func (output Output) Write(data any) error { + var formatted []byte + var err error + if output.outputYaml { + if output.verbose { + _, _ = internal.FPrintfReusingLine(output.errWriter, "Formatting as Yaml\r\n") + } + formatted, err = output.ToYaml(data) + } else { + if output.verbose { + _, _ = internal.FPrintfReusingLine(output.errWriter, "Formatting as JSON\r\n") + } + formatted, err = output.ToJson(data) + } + if err != nil { + return err + } + + if output.verbose { + if output.filename != "" { + _, _ = internal.FPrintfReusingLine(output.errWriter, "Writing to file: %s\r\n", output.filename) + } else { + _, _ = internal.FPrintfReusingLine(output.errWriter, "Writing output to writer\r\n") + } + } + bytes, err := output.writer.Write(formatted) + if err != nil { + log.WithError(err).Error("Error writing data to output") + return err + } + + if output.verbose { + if output.filename != "" { + log. + WithError(err). + WithFields(map[string]interface{}{ + "bytes": bytes, + "filename": output.filename, + }). + Debug("Wrote data") + } else { + log. + WithField("bytes", bytes). + Debug("Wrote data") + } + } + + err = output.writer.Flush() + if err != nil { + log. + WithError(err). + Error("Error flushing data to output") + return err + } + + return nil +} + +func (output Output) ToJson(data any) ([]byte, error) { + + jsonData, err := json.MarshalIndent(data, "", " ") + if err != nil { + log. + WithError(err). + Error("Error writing data as JSON") + return nil, err + } + + return jsonData, nil +} + +func (output Output) ToYaml(data any) ([]byte, error) { + + yamlData, err := yaml.Marshal(data) + if err != nil { + log. + WithError(err). + Error("Error writing data as Yaml") + return nil, err + } + + return yamlData, nil +} diff --git a/ziti/cmd/ascode/importer/importer.go b/ziti/cmd/ascode/importer/importer.go new file mode 100644 index 000000000..64818278e --- /dev/null +++ b/ziti/cmd/ascode/importer/importer.go @@ -0,0 +1,342 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 importer + +import ( + "encoding/json" + "errors" + "github.com/judedaryl/go-arrayutils" + "github.com/michaelquigley/pfxlog" + "github.com/openziti/edge-api/rest_management_api_client" + "github.com/openziti/ziti/internal" + "github.com/openziti/ziti/internal/rest/mgmt" + "github.com/openziti/ziti/ziti/cmd/edge" + "github.com/openziti/ziti/ziti/constants" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "gopkg.in/yaml.v3" + "io" + "os" + "strings" +) + +var log = pfxlog.Logger() + +type Importer struct { + loginOpts edge.LoginOptions + client *rest_management_api_client.ZitiEdgeManagement + reader Reader + ofJson bool + ofYaml bool + configCache map[string]any + serviceCache map[string]any + edgeRouterCache map[string]any + authPolicyCache map[string]any + extJwtSignersCache map[string]any + identityCache map[string]any +} + +func NewImportCmd(out io.Writer, errOut io.Writer) *cobra.Command { + + importer := &Importer{} + importer.loginOpts = edge.LoginOptions{} + + cmd := &cobra.Command{ + Use: "import filename [entity]", + Short: "Import entities", + Long: "Import all or selected entities from the specified file.\n" + + "Valid entities are: [all|ca/certificate-authority|identity|edge-router|service|config|config-type|service-policy|edge-router-policy|service-edge-router-policy|external-jwt-signer|auth-policy|posture-check] (default all)", + Args: cobra.MinimumNArgs(1), + PersistentPreRun: func(cmd *cobra.Command, args []string) { + importer.Init() + }, + Run: func(cmd *cobra.Command, args []string) { + data, err := importer.reader.read(args[0]) + if err != nil { + panic(errors.Join(errors.New("unable to read input"), err)) + } + m := map[string][]interface{}{} + + if importer.ofYaml { + err = yaml.Unmarshal(data, &m) + if err != nil { + panic(errors.Join(errors.New("unable to parse input data as yaml"), err)) + } + } else { + err = json.Unmarshal(data, &m) + if err != nil { + panic(errors.Join(errors.New("unable to parse input data as json"), err)) + } + } + + result, executeErr := importer.Execute(m, args[1:]) + if executeErr != nil { + panic(executeErr) + } + log.WithField("results", result).Debug("Finished") + }, + Hidden: true, + } + + v := viper.New() + + viper.SetEnvPrefix(constants.ZITI) // All env vars we seek will be prefixed with "ZITI_" + v.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + v.AutomaticEnv() + + cmd.Flags().SetInterspersed(true) + cmd.Flags().BoolVar(&importer.ofJson, "json", true, "Input parsed as JSON") + cmd.Flags().BoolVar(&importer.ofYaml, "yaml", false, "Input parsed as YAML") + cmd.MarkFlagsMutuallyExclusive("json", "yaml") + + edge.AddLoginFlags(cmd, &importer.loginOpts) + importer.loginOpts.Out = out + importer.loginOpts.Err = errOut + + return cmd +} + +func (importer *Importer) Init() { + + logLvl := logrus.InfoLevel + if importer.loginOpts.Verbose { + logLvl = logrus.DebugLevel + } + + pfxlog.GlobalInit(logLvl, pfxlog.DefaultOptions().Color()) + internal.ConfigureLogFormat(logLvl) + + client, err := mgmt.NewClient() + if err != nil { + loginErr := importer.loginOpts.Run() + if loginErr != nil { + log.Fatal(err) + } + client, err = mgmt.NewClient() + if err != nil { + log.Fatal(err) + } + } + importer.client = client + importer.reader = FileReader{} +} + +func (importer *Importer) Execute(data map[string][]interface{}, inputArgs []string) (map[string]any, error) { + + args := arrayutils.Map(inputArgs, strings.ToLower) + + importer.configCache = map[string]any{} + importer.serviceCache = map[string]any{} + importer.edgeRouterCache = map[string]any{} + importer.authPolicyCache = map[string]any{} + importer.extJwtSignersCache = map[string]any{} + importer.identityCache = map[string]any{} + + result := map[string]any{} + + cas := map[string]string{} + if importer.IsCertificateAuthorityImportRequired(args) { + log.Debug("Processing CertificateAuthorities") + var err error + cas, err = importer.ProcessCertificateAuthorities(data) + if err != nil { + return nil, err + } + log. + WithField("certificateAuthorities", cas). + Debug("CertificateAuthorities created") + } + result["certificateAuthorities"] = cas + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Created %d CertificateAuthorities\r\n", len(cas)) + + externalJwtSigners := map[string]string{} + if importer.IsExtJwtSignerImportRequired(args) { + log.Debug("Processing ExtJWTSigners") + var err error + externalJwtSigners, err = importer.ProcessExternalJwtSigners(data) + if err != nil { + return nil, err + } + log.WithField("externalJwtSigners", externalJwtSigners).Debug("ExtJWTSigners created") + } + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Created %d ExtJWTSigners\r\n", len(externalJwtSigners)) + result["externalJwtSigners"] = externalJwtSigners + + authPolicies := map[string]string{} + if importer.IsAuthPolicyImportRequired(args) { + log.Debug("Processing AuthPolicies") + var err error + authPolicies, err = importer.ProcessAuthPolicies(data) + if err != nil { + return nil, err + } + log.WithField("authPolicies", authPolicies).Debug("AuthPolicies created") + } + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Created %d AuthPolicies\r\n", len(authPolicies)) + result["authPolicies"] = authPolicies + + identities := map[string]string{} + if importer.IsIdentityImportRequired(args) { + log.Debug("Processing Identities") + var err error + identities, err = importer.ProcessIdentities(data) + if err != nil { + return nil, err + } + log.WithField("identities", identities).Debug("Identities created") + } + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Created %d Identities\r\n", len(identities)) + result["identities"] = identities + + configTypes := map[string]string{} + if importer.IsConfigTypeImportRequired(args) { + log.Debug("Processing ConfigTypes") + var err error + configTypes, err = importer.ProcessConfigTypes(data) + if err != nil { + return nil, err + } + log.WithField("configTypes", configTypes).Debug("ConfigTypes created") + } + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Created %d ConfigTypes\r\n", len(configTypes)) + result["configTypes"] = configTypes + + configs := map[string]string{} + if importer.IsConfigImportRequired(args) { + log.Debug("Processing Configs") + var err error + configs, err = importer.ProcessConfigs(data) + if err != nil { + return nil, err + } + log.WithField("configs", configs).Debug("Configs created") + } + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Created %d Configs\r\n", len(configs)) + result["configs"] = configs + + services := map[string]string{} + if importer.IsServiceImportRequired(args) { + log.Debug("Processing Services") + var err error + services, err = importer.ProcessServices(data) + if err != nil { + return nil, err + } + log.WithField("services", services).Debug("Services created") + } + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Created %d Services\r\n", len(services)) + result["services"] = services + + postureChecks := map[string]string{} + if importer.IsPostureCheckImportRequired(args) { + log.Debug("Processing PostureChecks") + var err error + postureChecks, err = importer.ProcessPostureChecks(data) + if err != nil { + return nil, err + } + log.WithField("postureChecks", postureChecks).Debug("PostureChecks created") + } + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Created %d PostureChecks\r\n", len(postureChecks)) + result["postureChecks"] = postureChecks + + routers := map[string]string{} + if importer.IsEdgeRouterImportRequired(args) { + log.Debug("Processing EdgeRouters") + var err error + routers, err = importer.ProcessEdgeRouters(data) + if err != nil { + return nil, err + } + log.WithField("edgeRouters", routers).Debug("EdgeRouters created") + } + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Created %d EdgeRouters\r\n", len(routers)) + result["edgeRouters"] = routers + + serviceEdgeRouterPolicies := map[string]string{} + if importer.IsServiceEdgeRouterPolicyImportRequired(args) { + log.Debug("Processing ServiceEdgeRouterPolicies") + var err error + serviceEdgeRouterPolicies, err = importer.ProcessServiceEdgeRouterPolicies(data) + if err != nil { + return nil, err + } + log.WithField("serviceEdgeRouterPolicies", serviceEdgeRouterPolicies).Debug("ServiceEdgeRouterPolicies created") + } + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Created %d ServiceEdgeRouterPolicies\r\n", len(serviceEdgeRouterPolicies)) + result["serviceEdgeRouterPolicies"] = serviceEdgeRouterPolicies + + servicePolicies := map[string]string{} + if importer.IsServicePolicyImportRequired(args) { + log.Debug("Processing ServicePolicies") + var err error + servicePolicies, err = importer.ProcessServicePolicies(data) + if err != nil { + return nil, err + } + log.WithField("servicePolicies", servicePolicies).Debug("ServicePolicies created") + } + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Created %d ServicePolicies\r\n", len(servicePolicies)) + result["servicePolicies"] = servicePolicies + + routerPolicies := map[string]string{} + if importer.IsEdgeRouterPolicyImportRequired(args) { + log.Debug("Processing EdgeRouterPolicies") + var err error + routerPolicies, err = importer.ProcessEdgeRouterPolicies(data) + if err != nil { + return nil, err + } + log.WithField("routerPolicies", routerPolicies).Debug("EdgeRouterPolicies created") + } + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Created %d EdgeRouterPolicies\r\n", len(routerPolicies)) + result["edgeRouterPolicies"] = routerPolicies + + log.Info("Upload complete") + + return result, nil +} + +func FromMap[T interface{}](input interface{}, v T) *T { + jsonData, _ := json.MarshalIndent(input, "", " ") + create := new(T) + err := json.Unmarshal(jsonData, &create) + if err != nil { + log. + WithField("err", err). + Error("error converting input to object") + return nil + } + return create +} + +type Reader interface { + read(input any) ([]byte, error) +} + +type FileReader struct { +} + +func (i FileReader) read(input any) ([]byte, error) { + file, err := os.ReadFile(input.(string)) + if err != nil { + return nil, err + } + + return file, nil +} diff --git a/ziti/cmd/ascode/importer/importer_auth_policies.go b/ziti/cmd/ascode/importer/importer_auth_policies.go new file mode 100644 index 000000000..b1faab7ef --- /dev/null +++ b/ziti/cmd/ascode/importer/importer_auth_policies.go @@ -0,0 +1,117 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 importer + +import ( + "encoding/json" + "github.com/Jeffail/gabs/v2" + "github.com/openziti/edge-api/rest_management_api_client/auth_policy" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/edge-api/rest_util" + "github.com/openziti/ziti/internal" + "github.com/openziti/ziti/internal/ascode" + "github.com/openziti/ziti/internal/rest/mgmt" + "slices" +) + +func (importer *Importer) IsAuthPolicyImportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "auth-policy") || + slices.Contains(args, "identity") +} + +func (importer *Importer) ProcessAuthPolicies(input map[string][]interface{}) (map[string]string, error) { + + if importer.loginOpts.Verbose { + log.Debug("Listing all AuthPolicies") + } + + result := map[string]string{} + + for _, data := range input["authPolicies"] { + create := FromMap(data, rest_model.AuthPolicyCreate{}) + + // see if the auth policy already exists + existing := mgmt.AuthPolicyFromFilter(importer.client, mgmt.NameFilter(*create.Name)) + if existing != nil { + if importer.loginOpts.Verbose { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "authPolicyId": *existing.ID, + }).Info("Found existing Auth Policy, skipping create") + } + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Skipping AuthPolicy %s\r", *create.Name) + continue + } + + // convert to a json doc so we can query inside the data + jsonData, _ := json.Marshal(data) + doc, jsonParseError := gabs.ParseJSON(jsonData) + if jsonParseError != nil { + log.WithError(jsonParseError).Error("Unable to parse json") + return nil, jsonParseError + } + allowedSigners := doc.Path("primary.extJwt.allowedSigners") + + // look up each signer by name and add to the create + allowedSignerIds := []string{} + for _, signer := range allowedSigners.Children() { + value := signer.Data().(string)[1:] + extJwtSigner, err := ascode.GetItemFromCache(importer.extJwtSignersCache, value, func(name string) (interface{}, error) { + return mgmt.ExternalJWTSignerFromFilter(importer.client, mgmt.NameFilter(name)), nil + }) + if err != nil { + log.WithField("name", *create.Name).Warn("Unable to read ExtJwtSigner") + return nil, err + } + allowedSignerIds = append(allowedSignerIds, *extJwtSigner.(*rest_model.ExternalJWTSignerDetail).ID) + } + create.Primary.ExtJWT.AllowedSigners = allowedSignerIds + + // do the actual create since it doesn't exist + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Creating AuthPolicy %s\r", *create.Name) + if importer.loginOpts.Verbose { + log.WithField("name", *create.Name). + Debug("Creating AuthPolicy") + } + created, createErr := importer.client.AuthPolicy.CreateAuthPolicy(&auth_policy.CreateAuthPolicyParams{AuthPolicy: create}, nil) + if createErr != nil { + if payloadErr, ok := createErr.(rest_util.ApiErrorPayload); ok { + log.WithFields(map[string]interface{}{ + "field": payloadErr.GetPayload().Error.Cause.APIFieldError.Field, + "reason": payloadErr.GetPayload().Error.Cause.APIFieldError.Reason, + "err": payloadErr, + }).Error("Unable to create AuthPolicy") + return nil, createErr + } else { + log.WithError(createErr).Error("Unable to create AuthPolicy") + return nil, createErr + } + } + + if importer.loginOpts.Verbose { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "authPolicyId": created.Payload.Data.ID, + }).Info("Created AuthPolicy") + } + + result[*create.Name] = created.Payload.Data.ID + } + + return result, nil +} diff --git a/ziti/cmd/ascode/importer/importer_certificate_authorities.go b/ziti/cmd/ascode/importer/importer_certificate_authorities.go new file mode 100644 index 000000000..5a9e46746 --- /dev/null +++ b/ziti/cmd/ascode/importer/importer_certificate_authorities.go @@ -0,0 +1,84 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 importer + +import ( + "github.com/openziti/edge-api/rest_management_api_client/certificate_authority" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/edge-api/rest_util" + "github.com/openziti/ziti/internal" + "github.com/openziti/ziti/internal/rest/mgmt" + "slices" +) + +func (importer *Importer) IsCertificateAuthorityImportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "ca") || + slices.Contains(args, "certificate-authority") +} + +func (importer *Importer) ProcessCertificateAuthorities(input map[string][]interface{}) (map[string]string, error) { + + var result = map[string]string{} + for _, data := range input["certificateAuthorities"] { + create := FromMap(data, rest_model.CaCreate{}) + + // see if the CA already exists + existing := mgmt.CertificateAuthorityFromFilter(importer.client, mgmt.NameFilter(*create.Name)) + if existing != nil { + if importer.loginOpts.Verbose { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "certificateAuthorityId": *existing.ID, + }). + Info("Found existing CertificateAuthority, skipping create") + } + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Skipping CertificateAuthority %s\r", *create.Name) + continue + } + + // do the actual create since it doesn't exist + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Creating CertificateAuthority %s\r", *create.Name) + created, createErr := importer.client.CertificateAuthority.CreateCa(&certificate_authority.CreateCaParams{Ca: create}, nil) + if createErr != nil { + if payloadErr, ok := createErr.(rest_util.ApiErrorPayload); ok { + log. + WithError(createErr). + WithFields(map[string]interface{}{ + "field": payloadErr.GetPayload().Error.Cause.APIFieldError.Field, + "reason": payloadErr.GetPayload().Error.Cause.APIFieldError.Reason, + }). + Error("Unable to create CertificateAuthority") + return nil, createErr + } else { + log.WithError(createErr).Error("Unable to create CertificateAuthority") + return nil, createErr + } + } + if importer.loginOpts.Verbose { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "certificateAuthorityId": created.Payload.Data.ID, + }). + Info("Created CertificateAuthority") + } + + result[*create.Name] = created.Payload.Data.ID + } + + return result, nil +} diff --git a/ziti/cmd/ascode/importer/importer_config_types.go b/ziti/cmd/ascode/importer/importer_config_types.go new file mode 100644 index 000000000..0c76d4c0f --- /dev/null +++ b/ziti/cmd/ascode/importer/importer_config_types.go @@ -0,0 +1,88 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 importer + +import ( + "github.com/openziti/edge-api/rest_management_api_client/config" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/edge-api/rest_util" + "github.com/openziti/ziti/internal" + "github.com/openziti/ziti/internal/rest/mgmt" + "slices" +) + +func (importer *Importer) IsConfigTypeImportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "config-type") || + slices.Contains(args, "config") || + slices.Contains(args, "service") +} + +func (importer *Importer) ProcessConfigTypes(input map[string][]interface{}) (map[string]string, error) { + + var result = map[string]string{} + for _, data := range input["configTypes"] { + create := FromMap(data, rest_model.ConfigTypeCreate{}) + + // see if the config type already exists + existing := mgmt.ConfigTypeFromFilter(importer.client, mgmt.NameFilter(*create.Name)) + if existing != nil { + if importer.loginOpts.Verbose { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "configTypeId": *existing.ID, + }). + Info("Found existing ConfigType, skipping create") + } + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Skipping ConfigType %s\r", *create.Name) + continue + } + + // do the actual create since it doesn't exist + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Creating ConfigType %s\r", *create.Name) + if importer.loginOpts.Verbose { + log.WithField("name", *create.Name). + Debug("Creating ConfigType") + } + created, createErr := importer.client.Config.CreateConfigType(&config.CreateConfigTypeParams{ConfigType: create}, nil) + if createErr != nil { + if payloadErr, ok := createErr.(rest_util.ApiErrorPayload); ok { + log.WithFields(map[string]interface{}{ + "field": payloadErr.GetPayload().Error.Cause.APIFieldError.Field, + "reason": payloadErr.GetPayload().Error.Cause.APIFieldError.Reason, + }). + Error("Unable to create ConfigType") + } else { + log.WithError(createErr). + Error("Unable to create ConfigType") + } + return nil, createErr + } + + if importer.loginOpts.Verbose { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "configTypeId": created.Payload.Data.ID, + }). + Info("Created Config Type") + } + + result[*create.Name] = created.Payload.Data.ID + } + + return result, nil +} diff --git a/ziti/cmd/ascode/importer/importer_configs.go b/ziti/cmd/ascode/importer/importer_configs.go new file mode 100644 index 000000000..69f391d98 --- /dev/null +++ b/ziti/cmd/ascode/importer/importer_configs.go @@ -0,0 +1,106 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 importer + +import ( + "encoding/json" + "errors" + "github.com/Jeffail/gabs/v2" + "github.com/openziti/edge-api/rest_management_api_client/config" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/edge-api/rest_util" + "github.com/openziti/ziti/internal" + "github.com/openziti/ziti/internal/ascode" + "github.com/openziti/ziti/internal/rest/mgmt" + "slices" +) + +func (importer *Importer) IsConfigImportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "config") || + slices.Contains(args, "service") +} + +func (importer *Importer) ProcessConfigs(input map[string][]interface{}) (map[string]string, error) { + + var result = map[string]string{} + for _, data := range input["configs"] { + create := FromMap(data, rest_model.ConfigCreate{}) + + // see if the config already exists + existing := mgmt.ConfigFromFilter(importer.client, mgmt.NameFilter(*create.Name)) + if existing != nil { + if importer.loginOpts.Verbose { + log. + WithFields(map[string]interface{}{ + "name": *create.Name, + "configId": *existing.ID, + }). + Info("Found existing Config, skipping create") + } + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Skipping Config %s\r", *create.Name) + continue + } + + // convert to a json doc so we can query inside the data + jsonData, _ := json.Marshal(data) + doc, jsonParseError := gabs.ParseJSON(jsonData) + if jsonParseError != nil { + log.WithError(jsonParseError).Error("Unable to parse json") + return nil, jsonParseError + } + + // look up the config type id from the name and add to the create + value := doc.Path("configType").Data().(string)[1:] + configType, _ := ascode.GetItemFromCache(importer.configCache, value, func(name string) (interface{}, error) { + return mgmt.ConfigTypeFromFilter(importer.client, mgmt.NameFilter(name)), nil + }) + if importer.configCache == nil { + return nil, errors.New("error reading ConfigType: " + value) + } + create.ConfigTypeID = configType.(*rest_model.ConfigTypeDetail).ID + + // do the actual create since it doesn't exist + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Creating Config %s\r", *create.Name) + if importer.loginOpts.Verbose { + log.WithField("name", *create.Name).Debug("Creating Config") + } + created, createErr := importer.client.Config.CreateConfig(&config.CreateConfigParams{Config: create}, nil) + if createErr != nil { + if payloadErr, ok := createErr.(rest_util.ApiErrorPayload); ok { + log.WithFields(map[string]interface{}{ + "field": payloadErr.GetPayload().Error.Cause.APIFieldError.Field, + "reason": payloadErr.GetPayload().Error.Cause.APIFieldError.Reason}). + Error("Unable to create Config") + return nil, createErr + } else { + log.WithError(createErr).Error("Unable to list Configs") + return nil, createErr + } + } + if importer.loginOpts.Verbose { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "configId": created.Payload.Data.ID, + }). + Info("Created Config") + } + result[*create.Name] = created.Payload.Data.ID + } + + return result, nil +} diff --git a/ziti/cmd/ascode/importer/importer_edgerouter_policies.go b/ziti/cmd/ascode/importer/importer_edgerouter_policies.go new file mode 100644 index 000000000..e0d63ee26 --- /dev/null +++ b/ziti/cmd/ascode/importer/importer_edgerouter_policies.go @@ -0,0 +1,96 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 importer + +import ( + "github.com/openziti/edge-api/rest_management_api_client/edge_router_policy" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/edge-api/rest_util" + "github.com/openziti/ziti/internal" + "github.com/openziti/ziti/internal/rest/mgmt" + "slices" +) + +func (importer *Importer) IsEdgeRouterPolicyImportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "edge-router-policy") +} + +func (importer *Importer) ProcessEdgeRouterPolicies(input map[string][]interface{}) (map[string]string, error) { + + var result = map[string]string{} + for _, data := range input["edgeRouterPolicies"] { + create := FromMap(data, rest_model.EdgeRouterPolicyCreate{}) + + // see if the router already exists + existing := mgmt.EdgeRouterPolicyFromFilter(importer.client, mgmt.NameFilter(*create.Name)) + if existing != nil { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "edgeRouterPolicyId": *existing.ID, + }). + Info("Found existing EdgeRouterPolicy, skipping create") + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Skipping EdgeRouterPolicy %s\r", *create.Name) + continue + } + + // look up the edgeRouter ids from the name and add to the create + edgeRouterRoles, err := importer.lookupEdgeRouters(create.EdgeRouterRoles) + if err != nil { + return nil, err + } + create.EdgeRouterRoles = edgeRouterRoles + + // look up the identity ids from the name and add to the create + identityRoles, err := importer.lookupIdentities(create.IdentityRoles) + if err != nil { + return nil, err + } + create.IdentityRoles = identityRoles + + // do the actual create since it doesn't exist + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Creating EdgeRouterPolicy %s\r", *create.Name) + if importer.loginOpts.Verbose { + log.WithField("name", *create.Name).Debug("Creating EdgeRouterPolicy") + } + created, createErr := importer.client.EdgeRouterPolicy.CreateEdgeRouterPolicy(&edge_router_policy.CreateEdgeRouterPolicyParams{Policy: create}, nil) + if createErr != nil { + if payloadErr, ok := createErr.(rest_util.ApiErrorPayload); ok { + log.WithFields(map[string]interface{}{ + "field": payloadErr.GetPayload().Error.Cause.APIFieldError.Field, + "reason": payloadErr.GetPayload().Error.Cause.APIFieldError.Reason, + }). + Error("Unable to create EdgeRouterPolicy") + return nil, createErr + } else { + log.WithError(createErr).Error("Unable to create EdgeRouterPolicy") + return nil, createErr + } + } + if importer.loginOpts.Verbose { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "routerPolicyId": created.Payload.Data.ID, + }). + Info("Created EdgeRouterPolicy") + } + + result[*create.Name] = created.Payload.Data.ID + } + + return result, nil +} diff --git a/ziti/cmd/ascode/importer/importer_edgerouters.go b/ziti/cmd/ascode/importer/importer_edgerouters.go new file mode 100644 index 000000000..36ec78301 --- /dev/null +++ b/ziti/cmd/ascode/importer/importer_edgerouters.go @@ -0,0 +1,103 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 importer + +import ( + "errors" + "github.com/openziti/edge-api/rest_management_api_client/edge_router" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/edge-api/rest_util" + "github.com/openziti/ziti/internal" + "github.com/openziti/ziti/internal/ascode" + "github.com/openziti/ziti/internal/rest/mgmt" + "slices" +) + +func (importer *Importer) IsEdgeRouterImportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "edge-router") || + slices.Contains(args, "er") +} + +func (importer *Importer) ProcessEdgeRouters(input map[string][]interface{}) (map[string]string, error) { + + var result = map[string]string{} + for _, data := range input["edgeRouters"] { + create := FromMap(data, rest_model.EdgeRouterCreate{}) + + // see if the router already exists + existing := mgmt.EdgeRouterFromFilter(importer.client, mgmt.NameFilter(*create.Name)) + if existing != nil { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "edgeRouterId": *existing.ID, + }). + Info("Found existing EdgeRouter, skipping create") + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Skipping EdgeRouter %s\r", *create.Name) + continue + } + + // do the actual create since it doesn't exist + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Creating EdgeRouterPolicy %s\r", *create.Name) + if importer.loginOpts.Verbose { + log.WithField("name", *create.Name).Debug("Creating EdgeRouter") + } + created, createErr := importer.client.EdgeRouter.CreateEdgeRouter(&edge_router.CreateEdgeRouterParams{EdgeRouter: create}, nil) + if createErr != nil { + if payloadErr, ok := createErr.(rest_util.ApiErrorPayload); ok { + log.WithFields(map[string]interface{}{ + "field": payloadErr.GetPayload().Error.Cause.APIFieldError.Field, + "reason": payloadErr.GetPayload().Error.Cause.APIFieldError.Reason, + }).Error("Unable to create EdgeRouter") + } else { + log.WithField("err", createErr).Error("Unable to create EdgeRouter") + return nil, createErr + } + } + if importer.loginOpts.Verbose { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "edgeRouterId": created.Payload.Data.ID, + }). + Info("Created EdgeRouter") + } + + result[*create.Name] = created.Payload.Data.ID + } + + return result, nil +} + +func (importer *Importer) lookupEdgeRouters(roles []string) ([]string, error) { + edgeRouterRoles := []string{} + for _, role := range roles { + if role[0:1] == "@" { + value := role[1:] + edgeRouter, _ := ascode.GetItemFromCache(importer.edgeRouterCache, value, func(name string) (interface{}, error) { + return mgmt.EdgeRouterFromFilter(importer.client, mgmt.NameFilter(name)), nil + }) + if edgeRouter == nil { + return nil, errors.New("error reading EdgeRouter: " + value) + } + edgeRouterId := edgeRouter.(*rest_model.EdgeRouterDetail).ID + edgeRouterRoles = append(edgeRouterRoles, "@"+*edgeRouterId) + } else { + edgeRouterRoles = append(edgeRouterRoles, role) + } + } + return edgeRouterRoles, nil +} diff --git a/ziti/cmd/ascode/importer/importer_external_jwt_signers.go b/ziti/cmd/ascode/importer/importer_external_jwt_signers.go new file mode 100644 index 000000000..6abdf40b1 --- /dev/null +++ b/ziti/cmd/ascode/importer/importer_external_jwt_signers.go @@ -0,0 +1,88 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 importer + +import ( + "github.com/openziti/edge-api/rest_management_api_client/external_jwt_signer" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/edge-api/rest_util" + "github.com/openziti/ziti/internal" + "github.com/openziti/ziti/internal/rest/mgmt" + "slices" +) + +func (importer *Importer) IsExtJwtSignerImportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "ext-jwt-signer") || + slices.Contains(args, "external-jwt-signer") || + slices.Contains(args, "auth-policy") || + slices.Contains(args, "identity") +} + +func (importer *Importer) ProcessExternalJwtSigners(input map[string][]interface{}) (map[string]string, error) { + + var result = map[string]string{} + for _, data := range input["externalJwtSigners"] { + create := FromMap(data, rest_model.ExternalJWTSignerCreate{}) + + // see if the signer already exists + existing := mgmt.ExternalJWTSignerFromFilter(importer.client, mgmt.NameFilter(*create.Name)) + if existing != nil { + if importer.loginOpts.Verbose { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "externalJwtSignerId": *existing.ID, + }). + Info("Found existing ExtJWTSigner, skipping create") + } + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Skipping ExtJWTSigner %s\r", *create.Name) + continue + } + + // do the actual create since it doesn't exist + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Creating ExtJWTSigner %s\r", *create.Name) + if importer.loginOpts.Verbose { + log.WithField("name", *create.Name).Debug("Creating ExtJWTSigner") + } + created, createErr := importer.client.ExternalJWTSigner.CreateExternalJWTSigner(&external_jwt_signer.CreateExternalJWTSignerParams{ExternalJWTSigner: create}, nil) + if createErr != nil { + if payloadErr, ok := createErr.(rest_util.ApiErrorPayload); ok { + log.WithFields(map[string]interface{}{ + "field": payloadErr.GetPayload().Error.Cause.APIFieldError.Field, + "reason": payloadErr.GetPayload().Error.Cause.APIFieldError.Reason, + "err": payloadErr, + }). + Error("Unable to create ExtJWTSigner") + return nil, createErr + } else { + log.Error("Unable to create ExtJWTSigner") + return nil, createErr + } + } + if importer.loginOpts.Verbose { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "externalJwtSignerId": created.Payload.Data.ID, + }). + Info("Created ExtJWTSigner") + } + + result[*create.Name] = created.Payload.Data.ID + } + + return result, nil +} diff --git a/ziti/cmd/ascode/importer/importer_identities.go b/ziti/cmd/ascode/importer/importer_identities.go new file mode 100644 index 000000000..af7acd980 --- /dev/null +++ b/ziti/cmd/ascode/importer/importer_identities.go @@ -0,0 +1,132 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 importer + +import ( + "encoding/json" + "errors" + "github.com/Jeffail/gabs/v2" + "github.com/openziti/edge-api/rest_management_api_client/identity" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/edge-api/rest_util" + "github.com/openziti/ziti/internal" + "github.com/openziti/ziti/internal/ascode" + "github.com/openziti/ziti/internal/rest/mgmt" + "slices" +) + +func (importer *Importer) IsIdentityImportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "identity") +} + +func (importer *Importer) ProcessIdentities(input map[string][]interface{}) (map[string]string, error) { + + var result = map[string]string{} + + for _, data := range input["identities"] { + create := FromMap(data, rest_model.IdentityCreate{}) + + existing := mgmt.IdentityFromFilter(importer.client, mgmt.NameFilter(*create.Name)) + if existing != nil { + if importer.loginOpts.Verbose { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "identityId": *existing.ID, + }). + Info("Found existing Identity, skipping create") + } + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Skipping Identity %s\r", *create.Name) + continue + } + + // set the type because it is not in the input + typ := rest_model.IdentityTypeDefault + create.Type = &typ + + // convert to a json doc so we can query inside the data + jsonData, _ := json.Marshal(data) + doc, jsonParseError := gabs.ParseJSON(jsonData) + if jsonParseError != nil { + log.WithError(jsonParseError).Error("Unable to parse json") + return nil, jsonParseError + } + policyName := doc.Path("authPolicy").Data().(string)[1:] + + // look up the auth policy id from the name and add to the create, omit if it's the "Default" policy + policy, _ := ascode.GetItemFromCache(importer.authPolicyCache, policyName, func(name string) (interface{}, error) { + return mgmt.AuthPolicyFromFilter(importer.client, mgmt.NameFilter(name)), nil + }) + if policy == nil { + return nil, errors.New("error reading Auth Policy: " + policyName) + } + if policy != "" && policy != "Default" { + create.AuthPolicyID = policy.(*rest_model.AuthPolicyDetail).ID + } + + // do the actual create since it doesn't exist + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Creating Identity %s\r", *create.Name) + created, createErr := importer.client.Identity.CreateIdentity(&identity.CreateIdentityParams{Identity: create}, nil) + if createErr != nil { + if payloadErr, ok := createErr.(rest_util.ApiErrorPayload); ok { + log.WithFields(map[string]interface{}{ + "field": payloadErr.GetPayload().Error.Cause.APIFieldError.Field, + "reason": payloadErr.GetPayload().Error.Cause.APIFieldError.Reason, + }). + Error("Unable to create Identity") + return nil, createErr + } else { + log.WithError(createErr).Error("Unable to create Identity") + return nil, createErr + } + } + if importer.loginOpts.Verbose { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "identityId": created.Payload.Data.ID, + }). + Info("Created identity") + } + + result[*create.Name] = created.Payload.Data.ID + } + + return result, nil +} + +func (importer *Importer) lookupIdentities(roles []string) ([]string, error) { + identityRoles := []string{} + for _, role := range roles { + if role[0:1] == "@" { + roleName := role[1:] + value, lookupErr := ascode.GetItemFromCache(importer.identityCache, roleName, func(name string) (interface{}, error) { + return mgmt.IdentityFromFilter(importer.client, mgmt.NameFilter(name)), nil + }) + if lookupErr != nil { + return nil, lookupErr + } + ident := value.(*rest_model.IdentityDetail) + if ident == nil { + return nil, errors.New("error reading Identity: " + roleName) + } + identityRoles = append(identityRoles, "@"+*ident.ID) + } else { + identityRoles = append(identityRoles, role) + } + } + return identityRoles, nil +} diff --git a/ziti/cmd/ascode/importer/importer_posture_check.go b/ziti/cmd/ascode/importer/importer_posture_check.go new file mode 100644 index 000000000..9e9472d0d --- /dev/null +++ b/ziti/cmd/ascode/importer/importer_posture_check.go @@ -0,0 +1,122 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 importer + +import ( + "encoding/json" + "github.com/Jeffail/gabs/v2" + "github.com/openziti/edge-api/rest_management_api_client/posture_checks" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/edge-api/rest_util" + "github.com/openziti/ziti/internal" + "github.com/openziti/ziti/internal/rest/mgmt" + "slices" + "strings" +) + +func (importer *Importer) IsPostureCheckImportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "posture-check") +} + +func (importer *Importer) ProcessPostureChecks(input map[string][]interface{}) (map[string]string, error) { + + var result = map[string]string{} + for _, data := range input["postureChecks"] { + + // convert to a json doc so we can query inside the data + jsonData, _ := json.Marshal(data) + doc, jsonParseError := gabs.ParseJSON(jsonData) + if jsonParseError != nil { + log.WithError(jsonParseError).Error("Unable to parse json") + return nil, jsonParseError + } + typeValue := doc.Path("typeId").Data().(string) + + var create rest_model.PostureCheckCreate + switch strings.ToUpper(typeValue) { + case string(rest_model.PostureCheckTypeDOMAIN): + create = FromMap(data, rest_model.PostureCheckDomainCreate{}) + case string(rest_model.PostureCheckTypeMAC): + create = FromMap(data, rest_model.PostureCheckMacAddressCreate{}) + case string(rest_model.PostureCheckTypeMFA): + create = FromMap(data, rest_model.PostureCheckMfaCreate{}) + case string(rest_model.PostureCheckTypeOS): + create = FromMap(data, rest_model.PostureCheckOperatingSystemCreate{}) + case string(rest_model.PostureCheckTypePROCESS): + create = FromMap(data, rest_model.PostureCheckProcessCreate{}) + case string(rest_model.PostureCheckTypePROCESSMULTI): + create = FromMap(data, rest_model.PostureCheckProcessMultiCreate{}) + default: + log.WithFields(map[string]interface{}{ + "name": *create.Name(), + "typeId": create.TypeID, + }). + Error("Unknown PostureCheck type") + } + + // see if the posture check already exists + existing := mgmt.PostureCheckFromFilter(importer.client, mgmt.NameFilter(*create.Name())) + if existing != nil { + if importer.loginOpts.Verbose { + log.WithFields(map[string]interface{}{ + "name": *create.Name(), + "postureCheckId": (*existing).ID(), + "typeId": create.TypeID(), + }). + Info("Found existing PostureCheck, skipping create") + } + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Skipping PostureCheck %s\r", *create.Name()) + continue + } + + // do the actual create since it doesn't exist + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Creating PostureCheck %s\r", *create.Name()) + if importer.loginOpts.Verbose { + log.WithFields(map[string]interface{}{ + "name": *create.Name(), + "typeId": create.TypeID(), + }). + Debug("Creating PostureCheck") + } + created, createErr := importer.client.PostureChecks.CreatePostureCheck(&posture_checks.CreatePostureCheckParams{PostureCheck: create}, nil) + if createErr != nil { + if payloadErr, ok := createErr.(rest_util.ApiErrorPayload); ok { + log.WithFields(map[string]interface{}{ + "field": payloadErr.GetPayload().Error.Cause.APIFieldError.Field, + "reason": payloadErr.GetPayload().Error.Cause.APIFieldError.Reason, + }). + Error("Unable to create PostureCheck") + } else { + log.WithError(createErr).Error("Unable to ") + return nil, createErr + } + } + if importer.loginOpts.Verbose { + log.WithFields(map[string]interface{}{ + "name": *create.Name(), + "postureCheckId": created.Payload.Data.ID, + "typeId": create.TypeID(), + }). + Info("Created PostureCheck") + } + + result[*create.Name()] = created.Payload.Data.ID + } + + return result, nil +} diff --git a/ziti/cmd/ascode/importer/importer_service_edgerouter_policies.go b/ziti/cmd/ascode/importer/importer_service_edgerouter_policies.go new file mode 100644 index 000000000..fa8293ee5 --- /dev/null +++ b/ziti/cmd/ascode/importer/importer_service_edgerouter_policies.go @@ -0,0 +1,97 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 importer + +import ( + "github.com/openziti/edge-api/rest_management_api_client/service_edge_router_policy" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/edge-api/rest_util" + "github.com/openziti/ziti/internal" + "github.com/openziti/ziti/internal/rest/mgmt" + "slices" +) + +func (importer *Importer) IsServiceEdgeRouterPolicyImportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "service-edge-router-policy") +} + +func (importer *Importer) ProcessServiceEdgeRouterPolicies(input map[string][]interface{}) (map[string]string, error) { + + var result = map[string]string{} + for _, data := range input["serviceEdgeRouterPolicies"] { + create := FromMap(data, rest_model.ServiceEdgeRouterPolicyCreate{}) + + // see if the service router policy already exists + existing := mgmt.ServiceEdgeRouterPolicyFromFilter(importer.client, mgmt.NameFilter(*create.Name)) + if existing != nil { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "serviceRouterPolicyId": *existing.ID, + }). + Info("Found existing ServiceEdgeRouterPolicy, skipping create") + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Skipping ServiceEdgeRouterPolicy %s\r", *create.Name) + continue + } + + // look up the service ids from the name and add to the create + serviceRoles, err := importer.lookupServices(create.ServiceRoles) + if err != nil { + return nil, err + } + create.ServiceRoles = serviceRoles + + // look up the edgeRouter ids from the name and add to the create + edgeRouterRoles, err := importer.lookupEdgeRouters(create.EdgeRouterRoles) + if err != nil { + return nil, err + } + create.EdgeRouterRoles = edgeRouterRoles + + // do the actual create since it doesn't exist + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Creating ServiceEdgeRouterPolicy %s\r", *create.Name) + if importer.loginOpts.Verbose { + log.WithField("name", *create.Name). + Debug("Creating ServiceEdgeRouterPolicy") + } + created, createErr := importer.client.ServiceEdgeRouterPolicy.CreateServiceEdgeRouterPolicy(&service_edge_router_policy.CreateServiceEdgeRouterPolicyParams{Policy: create}, nil) + if createErr != nil { + if payloadErr, ok := createErr.(rest_util.ApiErrorPayload); ok { + log.WithFields(map[string]interface{}{ + "field": payloadErr.GetPayload().Error.Cause.APIFieldError.Field, + "reason": payloadErr.GetPayload().Error.Cause.APIFieldError.Reason, + }). + Error("Unable to create ServiceEdgeRouterPolicy") + return nil, createErr + } else { + log.WithError(createErr).Error("Unable to create ServiceEdgeRouterPolicy") + return nil, createErr + } + } + if importer.loginOpts.Verbose { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "serviceEdgeRouterPolicyId": created.Payload.Data.ID, + }). + Info("Created ServiceEdgeRouterPolicy") + } + + result[*create.Name] = created.Payload.Data.ID + } + + return result, nil +} diff --git a/ziti/cmd/ascode/importer/importer_service_policies.go b/ziti/cmd/ascode/importer/importer_service_policies.go new file mode 100644 index 000000000..dd2458a31 --- /dev/null +++ b/ziti/cmd/ascode/importer/importer_service_policies.go @@ -0,0 +1,96 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 importer + +import ( + "errors" + "github.com/openziti/edge-api/rest_management_api_client/service_policy" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/edge-api/rest_util" + "github.com/openziti/ziti/internal" + "github.com/openziti/ziti/internal/rest/mgmt" + "slices" +) + +func (importer *Importer) IsServicePolicyImportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "service-policy") +} + +func (importer *Importer) ProcessServicePolicies(input map[string][]interface{}) (map[string]string, error) { + + var result = map[string]string{} + for _, data := range input["servicePolicies"] { + create := FromMap(data, rest_model.ServicePolicyCreate{}) + + // see if the service policy already exists + existing := mgmt.ServicePolicyFromFilter(importer.client, mgmt.NameFilter(*create.Name)) + if existing != nil { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "servicePolicyId": *existing.ID, + }). + Info("Found existing ServicePolicy, skipping create") + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Skipping ServicePolicy %s\r", *create.Name) + continue + } + + // look up the service ids from the name and add to the create + serviceRoles, err := importer.lookupServices(create.ServiceRoles) + if err != nil { + return nil, err + } + create.ServiceRoles = serviceRoles + + // look up the identity ids from the name and add to the create + identityRoles, err := importer.lookupIdentities(create.IdentityRoles) + if err != nil { + return nil, errors.Join(errors.New("Unable to read all identities from ServicePolicy"), err) + } + create.IdentityRoles = identityRoles + + // do the actual create since it doesn't exist + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Skipping ServicePolicy %s\r", *create.Name) + if importer.loginOpts.Verbose { + log.WithField("name", *create.Name).Debug("Creating ServicePolicy") + } + created, createErr := importer.client.ServicePolicy.CreateServicePolicy(&service_policy.CreateServicePolicyParams{Policy: create}, nil) + if createErr != nil { + if payloadErr, ok := createErr.(rest_util.ApiErrorPayload); ok { + log.WithFields(map[string]interface{}{ + "field": payloadErr.GetPayload().Error.Cause.APIFieldError.Field, + "reason": payloadErr.GetPayload().Error.Cause.APIFieldError.Reason, + }). + Error("Unable to create ServicePolicy") + } else { + log.WithError(createErr).Error("Unable to ") + return nil, createErr + } + } + if importer.loginOpts.Verbose { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "servicePolicyId": created.Payload.Data.ID, + }). + Info("Created ServicePolicy") + } + + result[*create.Name] = created.Payload.Data.ID + } + + return result, nil +} diff --git a/ziti/cmd/ascode/importer/importer_services.go b/ziti/cmd/ascode/importer/importer_services.go new file mode 100644 index 000000000..b00b76103 --- /dev/null +++ b/ziti/cmd/ascode/importer/importer_services.go @@ -0,0 +1,129 @@ +/* + Copyright NetFoundry Inc. + + 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 + + https://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 importer + +import ( + "encoding/json" + "errors" + "github.com/Jeffail/gabs/v2" + "github.com/openziti/edge-api/rest_management_api_client/service" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/edge-api/rest_util" + "github.com/openziti/ziti/internal" + "github.com/openziti/ziti/internal/ascode" + "github.com/openziti/ziti/internal/rest/mgmt" + "slices" +) + +func (importer *Importer) IsServiceImportRequired(args []string) bool { + return slices.Contains(args, "all") || len(args) == 0 || // explicit all or nothing specified + slices.Contains(args, "service") +} + +func (importer *Importer) ProcessServices(input map[string][]interface{}) (map[string]string, error) { + + var result = map[string]string{} + + for _, data := range input["services"] { + create := FromMap(data, rest_model.ServiceCreate{}) + + // see if the service already exists + existing := mgmt.ServiceFromFilter(importer.client, mgmt.NameFilter(*create.Name)) + if existing != nil { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "serviceId": *existing.ID, + }). + Info("Found existing Service, skipping create") + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Skipping Service %s\r", *create.Name) + continue + } + + // convert to a json doc so we can query inside the data + jsonData, _ := json.Marshal(data) + doc, jsonParseError := gabs.ParseJSON(jsonData) + if jsonParseError != nil { + log.WithError(jsonParseError).Error("Unable to parse json") + return nil, jsonParseError + } + configsNode := doc.Path("configs") + + // look up each config by name and add to the create + configIds := []string{} + for _, configName := range configsNode.Children() { + value := configName.Data().(string)[1:] + config, _ := ascode.GetItemFromCache(importer.configCache, value, func(name string) (interface{}, error) { + return mgmt.ConfigFromFilter(importer.client, mgmt.NameFilter(name)), nil + }) + if config == nil { + return nil, errors.New("error reading Config: " + value) + } + configIds = append(configIds, *config.(*rest_model.ConfigDetail).ID) + } + create.Configs = configIds + + // do the actual create since it doesn't exist + _, _ = internal.FPrintfReusingLine(importer.loginOpts.Err, "Creating Service %s\r", *create.Name) + if importer.loginOpts.Verbose { + log.WithField("name", *create.Name).Debug("Creating Service") + } + created, createErr := importer.client.Service.CreateService(&service.CreateServiceParams{Service: create}, nil) + if createErr != nil { + if payloadErr, ok := createErr.(rest_util.ApiErrorPayload); ok { + log.WithFields(map[string]interface{}{ + "field": payloadErr.GetPayload().Error.Cause.APIFieldError.Field, + "reason": payloadErr.GetPayload().Error.Cause.APIFieldError.Reason, + }). + Error("Unable to create Service") + } else { + log.WithError(createErr).Error("Unable to create Service") + return nil, createErr + } + } + if importer.loginOpts.Verbose { + log.WithFields(map[string]interface{}{ + "name": *create.Name, + "serviceId": created.Payload.Data.ID, + }). + Info("Created Service") + } + + result[*create.Name] = created.Payload.Data.ID + } + + return result, nil +} + +func (importer *Importer) lookupServices(roles []string) ([]string, error) { + serviceRoles := []string{} + for _, role := range roles { + if role[0:1] == "@" { + value := role[1:] + service, _ := ascode.GetItemFromCache(importer.serviceCache, value, func(name string) (interface{}, error) { + return mgmt.ServiceFromFilter(importer.client, mgmt.NameFilter(name)), nil + }) + if service == nil { + return nil, errors.New("error reading Service: " + value) + } + serviceId := service.(*rest_model.ServiceDetail).ID + serviceRoles = append(serviceRoles, "@"+*serviceId) + } else { + serviceRoles = append(serviceRoles, role) + } + } + return serviceRoles, nil +} diff --git a/ziti/cmd/ascode/importer/importer_test.go b/ziti/cmd/ascode/importer/importer_test.go new file mode 100644 index 000000000..173d95a3a --- /dev/null +++ b/ziti/cmd/ascode/importer/importer_test.go @@ -0,0 +1,223 @@ +package importer + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_InputArgs(t *testing.T) { + + importer := Importer{} + + t.Run("service import", func(t *testing.T) { + + assert.True(t, importer.IsCertificateAuthorityImportRequired([]string{"certificate-authority"}), "should be imported") + assert.True(t, importer.IsCertificateAuthorityImportRequired([]string{"all"}), "should be imported") + assert.True(t, importer.IsCertificateAuthorityImportRequired([]string{}), "should be imported") + + assert.False(t, importer.IsCertificateAuthorityImportRequired([]string{"service"}), "should not be imported") + assert.False(t, importer.IsCertificateAuthorityImportRequired([]string{"config"}), "should not be imported") + assert.False(t, importer.IsCertificateAuthorityImportRequired([]string{"config-type"}), "should not be imported") + assert.False(t, importer.IsCertificateAuthorityImportRequired([]string{"identity"}), "should not be imported") + assert.False(t, importer.IsCertificateAuthorityImportRequired([]string{"auth-policy"}), "should not be imported") + assert.False(t, importer.IsCertificateAuthorityImportRequired([]string{"ext-jwt-signer"}), "should not be imported") + assert.False(t, importer.IsCertificateAuthorityImportRequired([]string{"posture-check"}), "should not be imported") + assert.False(t, importer.IsCertificateAuthorityImportRequired([]string{"service-policy"}), "should not be imported") + assert.False(t, importer.IsCertificateAuthorityImportRequired([]string{"edge-router-policy"}), "should not be imported") + assert.False(t, importer.IsCertificateAuthorityImportRequired([]string{"service-edge-router-policy"}), "should not be imported") + + }) + + t.Run("service import", func(t *testing.T) { + + assert.True(t, importer.IsServiceImportRequired([]string{"service"}), "should be imported") + assert.True(t, importer.IsServiceImportRequired([]string{"all"}), "should be imported") + assert.True(t, importer.IsServiceImportRequired([]string{}), "should be imported") + + assert.False(t, importer.IsServiceImportRequired([]string{"config"}), "should not be imported") + assert.False(t, importer.IsServiceImportRequired([]string{"config-type"}), "should not be imported") + assert.False(t, importer.IsServiceImportRequired([]string{"identity"}), "should not be imported") + assert.False(t, importer.IsServiceImportRequired([]string{"auth-policy"}), "should not be imported") + assert.False(t, importer.IsServiceImportRequired([]string{"ext-jwt-signer"}), "should not be imported") + assert.False(t, importer.IsServiceImportRequired([]string{"posture-check"}), "should not be imported") + assert.False(t, importer.IsServiceImportRequired([]string{"service-policy"}), "should not be imported") + assert.False(t, importer.IsServiceImportRequired([]string{"edge-router-policy"}), "should not be imported") + assert.False(t, importer.IsServiceImportRequired([]string{"service-edge-router-policy"}), "should not be imported") + + }) + + t.Run("config import", func(t *testing.T) { + + assert.True(t, importer.IsConfigImportRequired([]string{"service"}), "should be imported") + assert.True(t, importer.IsConfigImportRequired([]string{"config"}), "should be imported") + assert.True(t, importer.IsConfigImportRequired([]string{"all"}), "should be imported") + assert.True(t, importer.IsConfigImportRequired([]string{}), "should be imported") + + assert.False(t, importer.IsConfigImportRequired([]string{"config-type"}), "should not be imported") + assert.False(t, importer.IsConfigImportRequired([]string{"identity"}), "should not be imported") + assert.False(t, importer.IsConfigImportRequired([]string{"certificate-authority"}), "should not be imported") + assert.False(t, importer.IsConfigImportRequired([]string{"auth-policy"}), "should not be imported") + assert.False(t, importer.IsConfigImportRequired([]string{"ext-jwt-signer"}), "should not be imported") + assert.False(t, importer.IsConfigImportRequired([]string{"posture-check"}), "should not be imported") + assert.False(t, importer.IsConfigImportRequired([]string{"service-policy"}), "should not be imported") + assert.False(t, importer.IsConfigImportRequired([]string{"edge-router-policy"}), "should not be imported") + assert.False(t, importer.IsConfigImportRequired([]string{"service-edge-router-policy"}), "should not be imported") + + }) + + t.Run("config-type import", func(t *testing.T) { + + assert.True(t, importer.IsConfigTypeImportRequired([]string{"service"}), "should be imported") + assert.True(t, importer.IsConfigTypeImportRequired([]string{"config"}), "should be imported") + assert.True(t, importer.IsConfigTypeImportRequired([]string{"config-type"}), "should be imported") + assert.True(t, importer.IsConfigTypeImportRequired([]string{"all"}), "should be imported") + assert.True(t, importer.IsConfigTypeImportRequired([]string{}), "should be imported") + + assert.False(t, importer.IsConfigTypeImportRequired([]string{"certificate-authority"}), "should not be imported") + assert.False(t, importer.IsConfigTypeImportRequired([]string{"identity"}), "should not be imported") + assert.False(t, importer.IsConfigTypeImportRequired([]string{"auth-policy"}), "should not be imported") + assert.False(t, importer.IsConfigTypeImportRequired([]string{"ext-jwt-signer"}), "should not be imported") + assert.False(t, importer.IsConfigTypeImportRequired([]string{"posture-check"}), "should not be imported") + assert.False(t, importer.IsConfigTypeImportRequired([]string{"service-policy"}), "should not be imported") + assert.False(t, importer.IsConfigTypeImportRequired([]string{"edge-router-policy"}), "should not be imported") + assert.False(t, importer.IsConfigTypeImportRequired([]string{"service-edge-router-policy"}), "should not be imported") + + }) + + t.Run("identity import", func(t *testing.T) { + + assert.True(t, importer.IsIdentityImportRequired([]string{"identity"}), "should be imported") + assert.True(t, importer.IsIdentityImportRequired([]string{"all"}), "should be imported") + assert.True(t, importer.IsIdentityImportRequired([]string{}), "should be imported") + + assert.False(t, importer.IsIdentityImportRequired([]string{"auth-policy"}), "should not be imported") + assert.False(t, importer.IsIdentityImportRequired([]string{"ext-jwt-signer"}), "should not be imported") + assert.False(t, importer.IsIdentityImportRequired([]string{"external-jwt-signer"}), "should not be imported") + assert.False(t, importer.IsIdentityImportRequired([]string{"certificate-authority"}), "should not be imported") + assert.False(t, importer.IsIdentityImportRequired([]string{"service"}), "should not be imported") + assert.False(t, importer.IsIdentityImportRequired([]string{"config"}), "should not be imported") + assert.False(t, importer.IsIdentityImportRequired([]string{"config-type"}), "should not be imported") + assert.False(t, importer.IsIdentityImportRequired([]string{"posture-check"}), "should not be imported") + assert.False(t, importer.IsIdentityImportRequired([]string{"service-policy"}), "should not be imported") + assert.False(t, importer.IsIdentityImportRequired([]string{"edge-router-policy"}), "should not be imported") + assert.False(t, importer.IsIdentityImportRequired([]string{"service-edge-router-policy"}), "should not be imported") + + }) + + t.Run("auth-policy import", func(t *testing.T) { + + assert.True(t, importer.IsAuthPolicyImportRequired([]string{"auth-policy"}), "should be imported") + assert.True(t, importer.IsAuthPolicyImportRequired([]string{"identity"}), "should be imported") + assert.True(t, importer.IsAuthPolicyImportRequired([]string{"all"}), "should be imported") + assert.True(t, importer.IsAuthPolicyImportRequired([]string{}), "should be imported") + + assert.False(t, importer.IsAuthPolicyImportRequired([]string{"ext-jwt-signer"}), "should not be imported") + assert.False(t, importer.IsAuthPolicyImportRequired([]string{"external-jwt-signer"}), "should not be imported") + assert.False(t, importer.IsAuthPolicyImportRequired([]string{"certificate-authority"}), "should not be imported") + assert.False(t, importer.IsAuthPolicyImportRequired([]string{"service"}), "should not be imported") + assert.False(t, importer.IsAuthPolicyImportRequired([]string{"config"}), "should not be imported") + assert.False(t, importer.IsAuthPolicyImportRequired([]string{"config-type"}), "should not be imported") + assert.False(t, importer.IsAuthPolicyImportRequired([]string{"posture-check"}), "should not be imported") + assert.False(t, importer.IsAuthPolicyImportRequired([]string{"service-policy"}), "should not be imported") + assert.False(t, importer.IsAuthPolicyImportRequired([]string{"edge-router-policy"}), "should not be imported") + assert.False(t, importer.IsAuthPolicyImportRequired([]string{"service-edge-router-policy"}), "should not be imported") + + }) + + t.Run("ext-jwt-signer import", func(t *testing.T) { + + assert.True(t, importer.IsExtJwtSignerImportRequired([]string{"ext-jwt-signer"}), "should be imported") + assert.True(t, importer.IsExtJwtSignerImportRequired([]string{"external-jwt-signer"}), "should be imported") + assert.True(t, importer.IsExtJwtSignerImportRequired([]string{"auth-policy"}), "should be imported") + assert.True(t, importer.IsExtJwtSignerImportRequired([]string{"identity"}), "should be imported") + assert.True(t, importer.IsExtJwtSignerImportRequired([]string{"all"}), "should be imported") + assert.True(t, importer.IsExtJwtSignerImportRequired([]string{}), "should be imported") + + assert.False(t, importer.IsExtJwtSignerImportRequired([]string{"certificate-authority"}), "should not be imported") + assert.False(t, importer.IsExtJwtSignerImportRequired([]string{"service"}), "should not be imported") + assert.False(t, importer.IsExtJwtSignerImportRequired([]string{"config"}), "should not be imported") + assert.False(t, importer.IsExtJwtSignerImportRequired([]string{"config-type"}), "should not be imported") + assert.False(t, importer.IsExtJwtSignerImportRequired([]string{"posture-check"}), "should not be imported") + assert.False(t, importer.IsExtJwtSignerImportRequired([]string{"service-policy"}), "should not be imported") + assert.False(t, importer.IsExtJwtSignerImportRequired([]string{"edge-router-policy"}), "should not be imported") + assert.False(t, importer.IsExtJwtSignerImportRequired([]string{"service-edge-router-policy"}), "should not be imported") + + }) + + t.Run("posture-check import", func(t *testing.T) { + + assert.True(t, importer.IsPostureCheckImportRequired([]string{"posture-check"}), "should be imported") + assert.True(t, importer.IsPostureCheckImportRequired([]string{"all"}), "should be imported") + assert.True(t, importer.IsPostureCheckImportRequired([]string{}), "should be imported") + + assert.False(t, importer.IsPostureCheckImportRequired([]string{"service"}), "should not be imported") + assert.False(t, importer.IsPostureCheckImportRequired([]string{"config"}), "should not be imported") + assert.False(t, importer.IsPostureCheckImportRequired([]string{"config-type"}), "should not be imported") + assert.False(t, importer.IsPostureCheckImportRequired([]string{"identity"}), "should not be imported") + assert.False(t, importer.IsPostureCheckImportRequired([]string{"auth-policy"}), "should not be imported") + assert.False(t, importer.IsPostureCheckImportRequired([]string{"ext-jwt-signer"}), "should not be imported") + assert.False(t, importer.IsPostureCheckImportRequired([]string{"external-jwt-signer"}), "should not be imported") + assert.False(t, importer.IsPostureCheckImportRequired([]string{"service-policy"}), "should not be imported") + assert.False(t, importer.IsPostureCheckImportRequired([]string{"service-edge-router-policy"}), "should not be imported") + assert.False(t, importer.IsPostureCheckImportRequired([]string{"edge-router-policy"}), "should not be imported") + + }) + + t.Run("service-policy import", func(t *testing.T) { + + assert.True(t, importer.IsServicePolicyImportRequired([]string{"service-policy"}), "should be imported") + assert.True(t, importer.IsServicePolicyImportRequired([]string{"all"}), "should be imported") + assert.True(t, importer.IsServicePolicyImportRequired([]string{}), "should be imported") + + assert.False(t, importer.IsServicePolicyImportRequired([]string{"service"}), "should not be imported") + assert.False(t, importer.IsServicePolicyImportRequired([]string{"config"}), "should not be imported") + assert.False(t, importer.IsServicePolicyImportRequired([]string{"config-type"}), "should not be imported") + assert.False(t, importer.IsServicePolicyImportRequired([]string{"identity"}), "should not be imported") + assert.False(t, importer.IsServicePolicyImportRequired([]string{"auth-policy"}), "should not be imported") + assert.False(t, importer.IsServicePolicyImportRequired([]string{"ext-jwt-signer"}), "should not be imported") + assert.False(t, importer.IsServicePolicyImportRequired([]string{"external-jwt-signer"}), "should not be imported") + assert.False(t, importer.IsServicePolicyImportRequired([]string{"posture-check"}), "should not be imported") + assert.False(t, importer.IsServicePolicyImportRequired([]string{"service-edge-router-policy"}), "should not be imported") + assert.False(t, importer.IsServicePolicyImportRequired([]string{"edge-router-policy"}), "should not be imported") + + }) + + t.Run("service-edge-router-policy import", func(t *testing.T) { + + assert.True(t, importer.IsServiceEdgeRouterPolicyImportRequired([]string{"service-edge-router-policy"}), "should be imported") + assert.True(t, importer.IsServiceEdgeRouterPolicyImportRequired([]string{"all"}), "should be imported") + assert.True(t, importer.IsServiceEdgeRouterPolicyImportRequired([]string{}), "should be imported") + + assert.False(t, importer.IsServiceEdgeRouterPolicyImportRequired([]string{"service"}), "should not be imported") + assert.False(t, importer.IsServiceEdgeRouterPolicyImportRequired([]string{"config"}), "should not be imported") + assert.False(t, importer.IsServiceEdgeRouterPolicyImportRequired([]string{"config-type"}), "should not be imported") + assert.False(t, importer.IsServiceEdgeRouterPolicyImportRequired([]string{"identity"}), "should not be imported") + assert.False(t, importer.IsServiceEdgeRouterPolicyImportRequired([]string{"auth-policy"}), "should not be imported") + assert.False(t, importer.IsServiceEdgeRouterPolicyImportRequired([]string{"ext-jwt-signer"}), "should not be imported") + assert.False(t, importer.IsServiceEdgeRouterPolicyImportRequired([]string{"external-jwt-signer"}), "should not be imported") + assert.False(t, importer.IsServiceEdgeRouterPolicyImportRequired([]string{"posture-check"}), "should not be imported") + assert.False(t, importer.IsServiceEdgeRouterPolicyImportRequired([]string{"service-policy"}), "should not be imported") + assert.False(t, importer.IsServiceEdgeRouterPolicyImportRequired([]string{"edge-router-policy"}), "should not be imported") + + }) + + t.Run("service-edge-router-policy import", func(t *testing.T) { + + assert.True(t, importer.IsEdgeRouterPolicyImportRequired([]string{"edge-router-policy"}), "should be imported") + assert.True(t, importer.IsEdgeRouterPolicyImportRequired([]string{"all"}), "should be imported") + assert.True(t, importer.IsEdgeRouterPolicyImportRequired([]string{}), "should be imported") + + assert.False(t, importer.IsEdgeRouterPolicyImportRequired([]string{"service"}), "should not be imported") + assert.False(t, importer.IsEdgeRouterPolicyImportRequired([]string{"config"}), "should not be imported") + assert.False(t, importer.IsEdgeRouterPolicyImportRequired([]string{"config-type"}), "should not be imported") + assert.False(t, importer.IsEdgeRouterPolicyImportRequired([]string{"identity"}), "should not be imported") + assert.False(t, importer.IsEdgeRouterPolicyImportRequired([]string{"auth-policy"}), "should not be imported") + assert.False(t, importer.IsEdgeRouterPolicyImportRequired([]string{"ext-jwt-signer"}), "should not be imported") + assert.False(t, importer.IsEdgeRouterPolicyImportRequired([]string{"external-jwt-signer"}), "should not be imported") + assert.False(t, importer.IsEdgeRouterPolicyImportRequired([]string{"posture-check"}), "should not be imported") + assert.False(t, importer.IsEdgeRouterPolicyImportRequired([]string{"service-policy"}), "should not be imported") + assert.False(t, importer.IsEdgeRouterPolicyImportRequired([]string{"service-edge-router-policy"}), "should not be imported") + + }) + +} diff --git a/ziti/cmd/ascode/test.yaml b/ziti/cmd/ascode/test.yaml new file mode 100644 index 000000000..42381b7b8 --- /dev/null +++ b/ziti/cmd/ascode/test.yaml @@ -0,0 +1,624 @@ +authPolicies: + - name: NetFoundry Console Integration Auth Policy + primary: + cert: + allowExpiredCerts: false + allowed: false + extJwt: + allowed: true + allowedSigners: + - '@NetFoundry Console Integration External JWT Signer' + updb: + allowed: false + lockoutDurationMinutes: 0 + maxAttempts: 0 + minPasswordLength: 5 + requireMixedCase: false + requireNumberChar: false + requireSpecialChar: false + secondary: + requireTotp: false + tags: + network-id: 25ba1aa3-4468-445a-910e-93f5b425f2c1 + resource-id: 0b511399-82f4-416c-a074-0e3aba711e22 + - name: Test123 + primary: + cert: + allowExpiredCerts: false + allowed: false + extJwt: + allowed: true + allowedSigners: [] + updb: + allowed: true + lockoutDurationMinutes: 0 + maxAttempts: 5 + minPasswordLength: 5 + requireMixedCase: false + requireNumberChar: false + requireSpecialChar: false + secondary: + requireTotp: false + tags: {} +certificateAuthorities: [] +configTypes: + - name: Empty Config Type + schema: null + tags: {} +configs: + - configType: '@intercept.v1' + data: + addresses: + - mylocal.com + portRanges: + - high: 547 + low: 547 + protocols: + - tcp + - udp + name: service2-intercept-config + tags: {} + - configType: '@intercept.v1' + data: + addresses: + - testservice.com + portRanges: + - high: 8083 + low: 8083 + protocols: + - tcp + - udp + name: ssssimple-intercept-config + tags: {} + - configType: '@host.v1' + data: + address: docker-ac-web-server-1 + allowedProtocols: + - tcp + forwardProtocol: true + port: 80 + name: ssssimple-host-config + tags: {} + - configType: '@host.v1' + data: + address: 127.0.0.1 + allowedProtocols: + - tcp + forwardProtocol: true + port: 123 + name: test-123-host-config + tags: {} + - configType: '@intercept.v1' + data: + addresses: + - jettylocal.com + portRanges: + - high: 8080 + low: 8080 + protocols: + - tcp + - udp + name: service1-intercept-config + tags: {} + - configType: '@host.v1' + data: + address: localhost + allowedProtocols: + - tcp + forwardProtocol: true + port: 8080 + name: service1-host-config + tags: {} + - configType: '@host.v1' + data: + address: localhost + allowedProtocols: + - tcp + forwardProtocol: true + port: 546 + name: service2-host-config + tags: {} + - configType: '@ziti-tunneler-client.v1' + data: + hostname: 192.168.242.2 + port: 502 + name: json + tags: {} + - configType: '@intercept.v1' + data: + addresses: + - simplesvctest1.ziti + portRanges: + - high: 321 + low: 321 + protocols: + - tcp + - udp + name: rg-test-123-intercept-config + tags: {} + - configType: '@intercept.v1' + data: + addresses: + - simplesvctest1.ziti + portRanges: + - high: 123 + low: 123 + protocols: + - tcp + - udp + name: test-123-intercept-config + tags: {} + - configType: '@host.v1' + data: + address: 127.0.0.1 + allowedProtocols: + - tcp + forwardProtocol: true + port: 321 + name: rg-test-123-host-config + tags: {} +edgeRouterPolicies: + - edgeRouterRoles: + - '@public-router1' + identityRoles: + - '@identity12' + name: routerpolicy1 + semantic: AnyOf + tags: {} + - edgeRouterRoles: + - '@custroutet2' + identityRoles: + - '@custroutet2' + name: edge-router-D98X8WmjYH-system + semantic: AnyOf + tags: {} + - edgeRouterRoles: + - '@asd' + identityRoles: + - '@asd' + name: edge-router-ORKiRq5WIU-system + semantic: AnyOf + tags: {} + - edgeRouterRoles: + - '@public-router1' + identityRoles: + - '@public-router1' + name: edge-router-Qo6blWsoY-system + semantic: AnyOf + tags: {} + - edgeRouterRoles: + - '@vis-customer-router' + identityRoles: + - '@vis-customer-router' + name: edge-router-w0OpEWmoY-system + semantic: AnyOf + tags: {} +externalJwtSigners: + - audience: https://gateway.staging.netfoundry.io/cloudziti/25ba1aa3-4468-445a-910e-93f5b425f2c1 + certPem: null + claimsProperty: sub + clientId: null + enabled: true + externalAuthUrl: null + fingerprint: null + issuer: https://netfoundry.io/jwt/NYFw7IGJKNP9AaG45iwCj + jwksEndpoint: https://gateway.staging.netfoundry.io/network-auth/v1/public/.well-known/NYFw7IGJKNP9AaG45iwCj/jwks.json + kid: null + name: NetFoundry Console Integration External JWT Signer + scopes: null + tags: {} + useExternalId: false +identities: + - authPolicy: '@Default' + disabled: false + edgeRouterConnectionStatus: null + externalId: null + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: vis-client + roleAttributes: + - client + tags: {} + typeId: Default + - authPolicy: '@NetFoundry Console Integration Auth Policy' + disabled: false + edgeRouterConnectionStatus: null + externalId: f1505b76-38ec-470b-9819-75984623c23d + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: Vinay Lakshmaiah + roleAttributes: [] + tags: {} + typeId: Default + - authPolicy: '@NetFoundry Console Integration Auth Policy' + disabled: false + edgeRouterConnectionStatus: null + externalId: 962818be-b8d5-4c37-8e5b-e5082aa4ddbf + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: harish donepudi + roleAttributes: [] + tags: {} + typeId: Default + - authPolicy: '@Default' + disabled: false + edgeRouterConnectionStatus: null + externalId: null + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: identity12 + roleAttributes: + - bind + tags: {} + typeId: Default + - authPolicy: '@Default' + disabled: false + edgeRouterConnectionStatus: null + externalId: null + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: Prashant + roleAttributes: [] + tags: {} + typeId: Default + - authPolicy: '@NetFoundry Console Integration Auth Policy' + disabled: false + edgeRouterConnectionStatus: null + externalId: 19fae786-dd8e-4d13-9864-588cee15be95 + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: Loren Fouts + roleAttributes: [] + tags: {} + typeId: Default + - authPolicy: '@NetFoundry Console Integration Auth Policy' + disabled: false + edgeRouterConnectionStatus: null + externalId: 1faad12a-1fc0-4d8a-a1e6-b2f993907017 + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: kenneth.bingham+kentest@netfoundry.io + roleAttributes: [] + tags: {} + typeId: Default + - authPolicy: '@NetFoundry Console Integration Auth Policy' + disabled: false + edgeRouterConnectionStatus: null + externalId: 8466464f-dcd4-46e9-a794-2bf68acc144b + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: ryan galletto + roleAttributes: [] + tags: {} + typeId: Default + - authPolicy: '@NetFoundry Console Integration Auth Policy' + disabled: false + edgeRouterConnectionStatus: null + externalId: 489ff3ee-52ec-11e8-aa95-12c0467c47be + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: Mike Guthrie + roleAttributes: [] + tags: {} + typeId: Default + - authPolicy: '@NetFoundry Console Integration Auth Policy' + disabled: false + edgeRouterConnectionStatus: null + externalId: 5f854c49-b5fd-44a1-b14f-a9c4aa5a7eba + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: 0dRsg5oRswW9gxv4H_8MX + roleAttributes: [] + tags: {} + typeId: Default + - authPolicy: '@Default' + disabled: false + edgeRouterConnectionStatus: null + externalId: null + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: Newone + roleAttributes: [] + tags: {} + typeId: Default + - authPolicy: '@NetFoundry Console Integration Auth Policy' + disabled: false + edgeRouterConnectionStatus: null + externalId: 356e0534-7fb8-4a48-b637-8a8821aa8035 + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: Jens Alm + roleAttributes: [] + tags: {} + typeId: Default + - authPolicy: '@NetFoundry Console Integration Auth Policy' + disabled: false + edgeRouterConnectionStatus: null + externalId: e37b3dc5-f375-4403-bf6d-e95a0a85d8e3 + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: mahesh eranna + roleAttributes: [] + tags: {} + typeId: Default + - authPolicy: '@Default' + disabled: false + edgeRouterConnectionStatus: null + externalId: null + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: admin2_user + roleAttributes: + - client + tags: {} + typeId: Default + - authPolicy: '@NetFoundry Console Integration Auth Policy' + disabled: false + edgeRouterConnectionStatus: null + externalId: e3b92baa-0530-4ad9-80fd-57e5b3283bf9 + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: Russell Allen + roleAttributes: [] + tags: {} + typeId: Default + - authPolicy: '@Default' + disabled: false + edgeRouterConnectionStatus: null + externalId: null + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: AnewAdmin + roleAttributes: + - client + tags: {} + typeId: Default + - authPolicy: '@NetFoundry Console Integration Auth Policy' + disabled: false + edgeRouterConnectionStatus: null + externalId: dab489a7-63d8-4e64-85b4-004ec0448037 + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: Prashant Savadi + roleAttributes: [] + tags: {} + typeId: Default + - authPolicy: '@NetFoundry Console Integration Auth Policy' + disabled: false + edgeRouterConnectionStatus: null + externalId: 988c2c94-f6dc-42ff-b1a1-8977915194db + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: Edward Moscardini + roleAttributes: [] + tags: {} + typeId: Default + - authPolicy: '@NetFoundry Console Integration Auth Policy' + disabled: false + edgeRouterConnectionStatus: null + externalId: 0ed768fa-7214-4404-8335-a715156dff45 + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: MOP Ziti Metrics Processor Service + roleAttributes: [] + tags: {} + typeId: Default + - authPolicy: '@NetFoundry Console Integration Auth Policy' + disabled: false + edgeRouterConnectionStatus: null + externalId: 468153e2-1f07-498e-999d-4511e3d3a771 + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: Tod Burtchell + roleAttributes: [] + tags: {} + typeId: Default + - authPolicy: '@Default' + disabled: false + edgeRouterConnectionStatus: null + externalId: null + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: rg-windows-dt + roleAttributes: [] + tags: + foo: bar + typeId: Default + - authPolicy: '@Default' + disabled: false + edgeRouterConnectionStatus: null + externalId: null + isDefaultAdmin: false + isMfaEnabled: false + isAdmin: false + name: Admin-Registered + roleAttributes: [] + tags: {} + typeId: Default +postureChecks: + - macAddresses: + - 0123456789ab + name: Mac + roleAttributes: + - mac + tags: {} + typeId: MAC + - name: OS + operatingSystems: + - type: macOS + versions: + - 10.3.1 + - 11.2.3 + roleAttributes: + - os + tags: {} + typeId: OS + - name: MFA + roleAttributes: + - mfa + tags: {} + timeoutSeconds: 30 + typeId: MFA + - name: Process + process: + hashes: null + osType: Linux + path: /path/something + roleAttributes: + - process + tags: {} + typeId: PROCESS +edgeRouters: + - appData: {} + disabled: false + enrollmentToken: 29818c67-fc39-4f7b-bb05-9db895719107 + hostname: "" + isTunnelerEnabled: true + name: custroutet2 + noTraversal: false + roleAttributes: + - vis-bind + tags: + hello: world + unverifiedCertPem: null + unverifiedFingerprint: null + - appData: {} + disabled: false + enrollmentToken: 5564c079-d089-4e4b-8c76-7fe57b1967da + hostname: "" + isTunnelerEnabled: true + name: asd + noTraversal: false + roleAttributes: [] + tags: {} + unverifiedCertPem: null + unverifiedFingerprint: null + - appData: {} + disabled: false + hostname: e08afdef-cb84-4a7e-b991-10a78575c2fc.staging.netfoundry.io + isTunnelerEnabled: true + name: public-router1 + noTraversal: false + roleAttributes: + - public + tags: {} + unverifiedCertPem: null + unverifiedFingerprint: null + - appData: {} + disabled: false + hostname: 565288cb-039e-47fa-b4d6-cb10d3864d14.staging.netfoundry.io + isTunnelerEnabled: false + name: enroll + noTraversal: false + roleAttributes: [] + tags: {} + unverifiedCertPem: null + unverifiedFingerprint: null + - appData: {} + disabled: false + hostname: c609f216-d095-4752-844e-52dd8fe022ea.staging.netfoundry.io + isTunnelerEnabled: false + name: nfhosted + noTraversal: false + roleAttributes: + - public + tags: {} + unverifiedCertPem: null + unverifiedFingerprint: null + - appData: + my: er + disabled: false + enrollmentToken: 43bf0e64-62c2-4c6a-a602-58b46c538471 + hostname: "" + isTunnelerEnabled: false + name: appdata + noTraversal: true + roleAttributes: [] + tags: {} + unverifiedCertPem: null + unverifiedFingerprint: null + - appData: {} + disabled: false + hostname: e6a40617-8394-49c1-82d3-ed78388551c5.staging.netfoundry.io + isTunnelerEnabled: true + name: vis-customer-router + noTraversal: false + roleAttributes: + - vis-bind + tags: {} + unverifiedCertPem: null + unverifiedFingerprint: null +serviceEdgeRouterPolicies: + - edgeRouterRoles: + - '@custroutet2' + name: ssep2 + semantic: AnyOf + serviceRoles: + - '@ssssimple' + tags: {} + - edgeRouterRoles: + - '@public-router1' + name: sep1 + semantic: AnyOf + serviceRoles: + - '@ssssimple' + tags: {} +servicePolicies: + - identityRoles: + - '@public-router1' + name: ssssimple-bind-policy + postureCheckRoles: null + semantic: AnyOf + serviceRoles: + - '@ssssimple' + tags: {} + type: Bind + - identityRoles: + - '@identity12' + name: ssssimple-dial-policy + postureCheckRoles: null + semantic: AnyOf + serviceRoles: + - '@ssssimple' + tags: {} + type: Dial +services: + - configs: [] + encryptionRequired: true + name: asdfasdf + roleAttributes: + - bcde + tags: {} + terminatorStrategy: smartrouting + - configs: + - '@ssssimple-intercept-config' + - '@ssssimple-host-config' + encryptionRequired: true + name: ssssimple + roleAttributes: + - abcd + - service + tags: + foo: bar + terminatorStrategy: smartrouting diff --git a/ziti/cmd/cmd.go b/ziti/cmd/cmd.go index dddeaa4f5..464db3ec2 100644 --- a/ziti/cmd/cmd.go +++ b/ziti/cmd/cmd.go @@ -19,6 +19,7 @@ package cmd import ( goflag "flag" "fmt" + "github.com/openziti/ziti/ziti/cmd/ascode/importer" "io" "os" "path/filepath" @@ -26,6 +27,7 @@ import ( "github.com/openziti/cobra-to-md" "github.com/openziti/ziti/ziti/cmd/agentcli" + "github.com/openziti/ziti/ziti/cmd/ascode/exporter" "github.com/openziti/ziti/ziti/cmd/common" "github.com/openziti/ziti/ziti/cmd/create" "github.com/openziti/ziti/ziti/cmd/database" @@ -136,7 +138,7 @@ func NewCmdRoot(in io.Reader, out, err io.Writer, cmd *cobra.Command) *cobra.Com demoCmd := demo.NewDemoCmd(p) opsCommands := &cobra.Command{ - Use: "ops", + Use: "ops", Short: "Various utilities useful when operating a Ziti network", } @@ -145,6 +147,8 @@ func NewCmdRoot(in io.Reader, out, err io.Writer, cmd *cobra.Command) *cobra.Com opsCommands.AddCommand(NewUnwrapIdentityFileCommand(out, err)) opsCommands.AddCommand(verify.NewVerifyNetwork(out, err)) opsCommands.AddCommand(verify.NewVerifyTraffic(out, err)) + opsCommands.AddCommand(exporter.NewExportCmd(out, err)) + opsCommands.AddCommand(importer.NewImportCmd(out, err)) groups := templates.CommandGroups{ { diff --git a/ziti/cmd/edge/quickstart.go b/ziti/cmd/edge/quickstart.go index ef611c973..51c386b04 100644 --- a/ziti/cmd/edge/quickstart.go +++ b/ziti/cmd/edge/quickstart.go @@ -200,6 +200,7 @@ func (o *QuickstartOpts) run(ctx context.Context) { tmpDir, _ := os.MkdirTemp("", "quickstart") o.Home = tmpDir o.cleanOnExit = true + logrus.Infof("temporary --home '%s'", o.Home) } else { //normalize path if strings.HasPrefix(o.Home, "~") { diff --git a/ziti/cmd/verify/ops_verify_network.go b/ziti/cmd/verify/ops_verify_network.go index e6599248f..8fe7a405e 100644 --- a/ziti/cmd/verify/ops_verify_network.go +++ b/ziti/cmd/verify/ops_verify_network.go @@ -13,10 +13,12 @@ See the License for the specific language governing permissions and limitations under the License. */ + package verify import ( "fmt" + "github.com/openziti/ziti/internal" "io" "net" "os" @@ -36,7 +38,7 @@ var log = pfxlog.Logger() type network struct { controllerConfig string routerConfig string - verbose bool + verbose bool } type protoHostPort struct { @@ -63,7 +65,7 @@ func NewVerifyNetwork(_ io.Writer, _ io.Writer) *cobra.Command { } pfxlog.GlobalInit(logLvl, pfxlog.DefaultOptions().Color()) - configureLogFormat(logLvl) + internal.ConfigureLogFormat(logLvl) anyFailure := false if n.controllerConfig != "" { diff --git a/ziti/cmd/verify/ops_verify_traffic.go b/ziti/cmd/verify/ops_verify_traffic.go index b22fd4f4d..2e0ff7b63 100644 --- a/ziti/cmd/verify/ops_verify_traffic.go +++ b/ziti/cmd/verify/ops_verify_traffic.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "github.com/michaelquigley/pfxlog" + "github.com/openziti/ziti/internal" "github.com/openziti/ziti/ziti/cmd/edge" "github.com/sirupsen/logrus" "io" @@ -71,7 +72,7 @@ func NewVerifyTraffic(out io.Writer, errOut io.Writer) *cobra.Command { } pfxlog.GlobalInit(logLvl, pfxlog.DefaultOptions().Color()) - configureLogFormat(logLvl) + internal.ConfigureLogFormat(logLvl) timePrefix := time.Now().Format("2006-01-02-1504") if t.prefix == "" { @@ -133,7 +134,7 @@ func NewVerifyTraffic(out io.Writer, errOut io.Writer) *cobra.Command { cmd.Flags().BoolVar(&t.allowMultipleServers, "allow-multiple-servers", false, "Whether to allows the same server multiple times.") cmd.Flags().BoolVar(&t.haEnabled, "ha", false, "Enable high availability mode.") _ = cmd.Flags().MarkHidden("ha") - + edge.AddLoginFlags(cmd, &t.loginOpts) t.loginOpts.Out = out t.loginOpts.Err = errOut