diff --git a/controller/model/testing.go b/controller/model/testing.go index 7ac47f2ea..d2dcf95e5 100644 --- a/controller/model/testing.go +++ b/controller/model/testing.go @@ -383,29 +383,3 @@ func (self *testConfig) GetVersionProvider() versions.VersionProvider { func (self *testConfig) GetCloseNotify() <-chan struct{} { return self.closeNotify } - -// -//type testDbProvider struct { -// ctx *TestContext -//} -// -//func (p *testDbProvider) GetDb() boltz.Db { -// return p.ctx.GetDb() -//} -// -//func (p *testDbProvider) GetStores() *Stores { -// return p.ctx.n.GetStores() -//} -// -//func (p *testDbProvider) GetServiceCache() network.Cache { -// return p -//} -// -//func (p *testDbProvider) NotifyRouterRenamed(_, _ string) {} -// -//func (p *testDbProvider) RemoveFromCache(_ string) { -//} -// -//func (p *testDbProvider) GetManagers() *network.Managers { -// return p.ctx.n.Managers -//} diff --git a/zititest/go.mod b/zititest/go.mod index b3be62506..51a605a19 100644 --- a/zititest/go.mod +++ b/zititest/go.mod @@ -15,13 +15,14 @@ require ( github.com/openziti/agent v1.0.16 github.com/openziti/channel/v2 v2.0.125 github.com/openziti/edge-api v0.26.14 - github.com/openziti/fablab v0.5.42 + github.com/openziti/fablab v0.5.49 github.com/openziti/foundation/v2 v2.0.42 github.com/openziti/identity v1.0.74 github.com/openziti/sdk-golang v0.23.15 github.com/openziti/storage v0.2.34 github.com/openziti/transport/v2 v2.0.128 github.com/openziti/ziti v0.28.3 + github.com/orcaman/concurrent-map/v2 v2.0.1 github.com/pkg/errors v0.9.1 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 github.com/sirupsen/logrus v1.9.3 @@ -143,7 +144,6 @@ require ( github.com/openziti/x509-claims v1.0.3 // indirect github.com/openziti/xweb/v2 v2.1.0 // indirect github.com/openziti/ziti-db-explorer v1.1.3 // indirect - github.com/orcaman/concurrent-map/v2 v2.0.1 // indirect github.com/parallaxsecond/parsec-client-go v0.0.0-20221025095442-f0a77d263cf9 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pkg/sftp v1.13.6 // indirect diff --git a/zititest/go.sum b/zititest/go.sum index c1b71b784..1d35197bb 100644 --- a/zititest/go.sum +++ b/zititest/go.sum @@ -596,8 +596,8 @@ github.com/openziti/dilithium v0.3.3 h1:PLgQ6PMNLSTzCFbX/h98cmudgz/cU6TmjdSv5NAP github.com/openziti/dilithium v0.3.3/go.mod h1:vsCjI2AU/hon9e+dLhUFbCNGesJDj2ASgkySOcpmvjo= github.com/openziti/edge-api v0.26.14 h1:BpxKavDVdykOY7GSdKXi7klv9Im4vw7bTMujz3wFE1Q= github.com/openziti/edge-api v0.26.14/go.mod h1:aTRqB9J/6ISbw9+YWjzD0tvWAXY98m8yWcbiGNqAQTU= -github.com/openziti/fablab v0.5.42 h1:vENJKfEba2T4sSLwlKDL/IzBYfY8iHnhc4umf6IESiY= -github.com/openziti/fablab v0.5.42/go.mod h1:HDT06y1QX8kO8ZQrgHvZmJsvc8iRybESGtlDLDII4ks= +github.com/openziti/fablab v0.5.49 h1:uYmBQBS7auPeUlQBpnrU+ktDFhUTOklwwftBBqXcaDk= +github.com/openziti/fablab v0.5.49/go.mod h1:TAXN6lhTnmjL4nwkio3Ql96Lf5gnyeSbm7uV+CilsPo= github.com/openziti/foundation/v2 v2.0.42 h1:cXZFql3xY92AHPCk/wqIoSUX9EHgegwPrs5bhFE2jNg= github.com/openziti/foundation/v2 v2.0.42/go.mod h1:a+REYnK9bZ2cvmOiuBLp57MjQqn3U5cHhJcDMLMO8rE= github.com/openziti/identity v1.0.74 h1:BUCKjGXIdoEHLL1+kO/smitZvGwP9dOUpjVEJFtSSBc= diff --git a/zititest/models/db-ctrl-test/configs/ctrl.yml.tmpl b/zititest/models/db-ctrl-test/configs/ctrl.yml.tmpl new file mode 100644 index 000000000..430638ecf --- /dev/null +++ b/zititest/models/db-ctrl-test/configs/ctrl.yml.tmpl @@ -0,0 +1,194 @@ +v: 3 + +db: /home/{{ .Model.MustVariable "credentials.ssh.username" }}/ctrl.db + +identity: + cert: /home/{{ .Model.MustVariable "credentials.ssh.username" }}/fablab/pki/{{ .Component.Id }}/certs/{{ .Component.Id }}-server.chain.pem + key: /home/{{ .Model.MustVariable "credentials.ssh.username" }}/fablab/pki/{{ .Component.Id }}/keys/{{ .Component.Id }}-server.key + ca: /home/{{ .Model.MustVariable "credentials.ssh.username" }}/fablab/pki/{{ .Component.Id }}/certs/{{ .Component.Id }}.chain.pem + +network: + enableLegacyLinkMgmt: true + +tls: + handshakeTimeout: 30s + +# the endpoint that routers will connect to the controller over. +ctrl: + listener: tls:0.0.0.0:6262 + options: + advertiseAddress: tls:{{ .Host.PublicIp }}:6262 + # (optional) settings + # set the maximum number of connect requests that are buffered and waiting to be acknowledged (1 to 5000, default 1000) + #maxQueuedConnects: 50 + + # the maximum number of connects that have begun hello synchronization (1 to 1000, default 16) + #maxOutstandingConnects: 100 + + # the number of milliseconds to wait before a hello synchronization fails and closes the connection (30ms to 60000ms, default: 1000ms) + #connectTimeoutMs: 3000 + + # Sets the control channel write timeout. A write timeout will close the control channel, so the router will reconnect + #writeTimeout: 15s + + # A listener address which will be sent to connecting routers in order to change their configured controller + # address. If defined, routers will update address configuration to immediately use the new address for future + # connections. The value of newListener must be resolvable both via DNS and validate via certificates + #newListener: tls:localhost:6262 + +events: + jsonLogger: + subscriptions: +# - type: fabric.routers +# - type: fabric.terminators +# - type: metrics +# sourceFilter: .* +# metricFilter: .*egress.*m1_rate* +# - type: fabric.circuits +# include: +# - created +# - type: edge.sessions +# include: +# - created +# - type: edge.apiSessions +# - type: fabric.usage +# - type: services +# - type: fabric.usage + - type: edge.entityCounts + interval: 1m + handler: + type: file + format: json + path: /home/ubuntu/logs/events.json + +healthChecks: + boltCheck: + # How often to try entering a bolt read tx. Defaults to 30 seconds + interval: 30s + # When to timeout the check. Defaults to 15 seconds + timeout: 15s + # How long to wait before starting the check. Defaults to 15 seconds + initialDelay: 15s + +# By having an 'edge' section defined, the ziti-controller will attempt to parse the edge configuration. Removing this +# section, commenting out, or altering the name of the section will cause the edge to not run. +edge: + # This section represents the configuration of the Edge API that is served over HTTPS + api: + #(optional, default 90s) Alters how frequently heartbeat and last activity values are persisted + # activityUpdateInterval: 90s + #(optional, default 250) The number of API Sessions updated for last activity per transaction + # activityUpdateBatchSize: 250 + # sessionTimeout - optional, default 10m + # The number of minutes before an Edge API session will timeout. Timeouts are reset by + # API requests and connections that are maintained to Edge Routers + sessionTimeout: 30m + # address - required + # The default address (host:port) to use for enrollment for the Client API. This value must match one of the addresses + # defined in a bind point's address field for the `edge-client` API in the web section. + address: {{ .Host.PublicIp }}:1280 + # enrollment - required + # A section containing settings pertaining to enrollment. + enrollment: + # signingCert - required + # A Ziti Identity configuration section that specifically makes use of the cert and key fields to define + # a signing certificate from the PKI that the Ziti environment is using to sign certificates. The signingCert.cert + # will be added to the /.well-known CA store that is used to bootstrap trust with the Ziti Controller. + signingCert: + cert: /home/{{ .Model.MustVariable "credentials.ssh.username" }}/fablab/pki/{{ .Component.Id }}/certs/{{ .Component.Id }}.cert + key: /home/{{ .Model.MustVariable "credentials.ssh.username" }}/fablab/pki/{{ .Component.Id }}/keys/{{ .Component.Id }}.key + + # edgeIdentity - optional + # A section for identity enrollment specific settings + edgeIdentity: + # duration - optional, default 5m + # The length of time that a Ziti Edge Identity enrollment should remain valid. After + # this duration, the enrollment will expire and not longer be usable. + duration: 1h + # edgeRouter - Optional + # A section for edge router enrollment specific settings. + edgeRouter: + # duration - optional, default 5m + # The length of time that a Ziti Edge Router enrollment should remain valid. After + # this duration, the enrollment will expire and not longer be usable. + duration: 1h + + +# web - optional +# Defines webListeners that will be hosted by the controller. Each webListener can host many APIs and be bound to many +# bind points. +web: + # name - required + # Provides a name for this listener, used for logging output. Not required to be unique, but is highly suggested. + - name: all-apis-localhost + # bindPoints - required + # One or more bind points are required. A bind point specifies an interface (interface:port string) that defines + # where on the host machine the webListener will listen and the address (host:port) that should be used to + # publicly address the webListener(i.e. mydomain.com, localhost, 127.0.0.1). This public address may be used for + # incoming address resolution as well as used in responses in the API. + bindPoints: + #interface - required + # A host:port string on which network interface to listen on. 0.0.0.0 will listen on all interfaces + - interface: 0.0.0.0:1280 + + # address - required + # The public address that external incoming requests will be able to resolve. Used in request processing and + # response content that requires full host:port/path addresses. + address: {{ .Host.PublicIp }}:1280 + + # newAddress - optional + # A host:port string which will be sent out as an HTTP header "ziti-new-address" if specified. If the header + # is present, clients should update location configuration to immediately use the new address for future + # connections. The value of newAddress must be resolvable both via DNS and validate via certificates + #newAddress: localhost:1280 + # identity - optional + # Allows the webListener to have a specific identity instead of defaulting to the root `identity` section. + # identity: + # cert: ${ZITI_SOURCE}/ziti/etc/ca/intermediate/certs/ctrl-client.cert.pem + # server_cert: ${ZITI_SOURCE}/ziti/etc/ca/intermediate/certs/ctrl-server.cert.pem + # key: ${ZITI_SOURCE}/ziti/etc/ca/intermediate/private/ctrl.key.pem + # ca: ${ZITI_SOURCE}/ziti/etc/ca/intermediate/certs/ca-chain.cert.pem + # options - optional + # Allows the specification of webListener level options - mainly dealing with HTTP/TLS settings. These options are + # used for all http servers started by the current webListener. + options: + # idleTimeout - optional, default 5000ms + # The maximum amount of idle time in milliseconds allowed for pipelined HTTP requests. Setting this too high + # can cause resources on the host to be consumed as clients remain connected and idle. Lowering this value + # will cause clients to reconnect on subsequent HTTPs requests. + idleTimeout: 5000ms #http timeouts, new + + # readTimeout - optional, default 5000ms + # The maximum amount of time in milliseconds http servers will wait to read the first incoming requests. A higher + # value risks consuming resources on the host with clients that are acting bad faith or suffering from high latency + # or packet loss. A lower value can risk losing connections to high latency/packet loss clients. + + readTimeout: 5000ms + # writeTimeout - optional, default 10000ms + # The total maximum time in milliseconds that the http server will wait for a single requests to be received and + # responded too. A higher value can allow long running requests to consume resources on the host. A lower value + # can risk ending requests before the server has a chance to respond. + + writeTimeout: 100000ms + # minTLSVersion - optional, default TSL1.2 + # The minimum version of TSL to support + + minTLSVersion: TLS1.2 + # maxTLSVersion - optional, default TSL1.3 + # The maximum version of TSL to support + + maxTLSVersion: TLS1.3 + # apis - required + # Allows one or more APIs to be bound to this webListener + apis: + # binding - required + # Specifies an API to bind to this webListener. Built-in APIs are + # - health-checks + # - edge-management + # - edge-client + # - fabric-management + - binding: health-checks + - binding: fabric + - binding: edge-management + - binding: edge-client + - binding: edge-oidc \ No newline at end of file diff --git a/zititest/models/db-ctrl-test/configs/router.yml.tmpl b/zititest/models/db-ctrl-test/configs/router.yml.tmpl new file mode 100644 index 000000000..af00778de --- /dev/null +++ b/zititest/models/db-ctrl-test/configs/router.yml.tmpl @@ -0,0 +1,78 @@ +{{$ssh_username := .Model.MustVariable "credentials.ssh.username"}} +{{$identity := .Component.Id}} +{{$router_ip := .Host.PublicIp}} + +v: 3 + +enableDebugOps: true + +identity: + cert: /home/{{$ssh_username}}/router-pki/{{$identity}}-client.cert + server_cert: /home/{{$ssh_username}}/router-pki/{{$identity}}-server.cert + key: /home/{{$ssh_username}}/router-pki/{{$identity}}.key + ca: /home/{{$ssh_username}}/router-pki/{{$identity}}-server.chain.pem + +tls: + handshakeTimeout: 30s + +ctrl: + endpoints: {{ range $host := .Model.MustSelectHosts "component.ctrl" 1 }} + - tls:{{ $host.PublicIp }}:6262{{end}} + startupTimeout: 5m + +healthChecks: + ctrlPingCheck: + # How often to ping the controller over the control channel. Defaults to 30 seconds + interval: 30s + # When to timeout the ping. Defaults to 15 seconds + timeout: 15s + # How long to wait before pinging the controller. Defaults to 15 seconds + initialDelay: 15s + +metrics: + reportInterval: 15s + messageQueueSize: 10 + +link: +{{if .Component.HasTag "public"}} + listeners: + - binding: transport + bind: tls:0.0.0.0:60{{printf "%02d" .Component.ScaleIndex }} + advertise: tls:{{$router_ip}}:60{{printf "%02d" .Component.ScaleIndex }} +{{end}} + dialers: + - binding: transport + options: + connectTimeout: 30s + +listeners: +{{if .Component.HasTag "tunneler"}} + - binding: tunnel + options: + mode: host +{{end}} + - binding: edge + address: tls:0.0.0.0:62{{printf "%02d" .Component.ScaleIndex }} + options: + # (required) The public hostname and port combination that Ziti SDKs should connect on. Previously this was in the chanIngress section. + advertise: {{ .Host.PublicIp }}:62{{printf "%02d" .Component.ScaleIndex }} + +# By having an 'edge' section defined, the ziti-router will attempt to parse the edge configuration. Removing this +# section, commenting out, or altering the name of the section will cause the router to no longer operate as an Edge +# Router. +edge: + # (required) Information used to generate the initial registration CSR. For documentation on these fields please + # refer to the openssl documentation. These values MUST be supplied and have no defaults. + csr: + country: US + province: NC + locality: Charlotte + organization: NetFoundry + organizationalUnit: Ziti + + # (required) SANs that this Gateways certs should contain. At least one IP or DNS SAN should be defined that matches + # the edge listeners "advertise" value from the "listeners" section. + sans: + ip: + - {{ .Host.PublicIp }} + diff --git a/zititest/models/db-ctrl-test/main.go b/zititest/models/db-ctrl-test/main.go new file mode 100644 index 000000000..9d14e3d25 --- /dev/null +++ b/zititest/models/db-ctrl-test/main.go @@ -0,0 +1,382 @@ +/* + Copyright 2019 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 main + +import ( + "bufio" + "embed" + _ "embed" + "fmt" + "github.com/openziti/fablab" + "github.com/openziti/fablab/kernel/lib/actions" + "github.com/openziti/fablab/kernel/lib/actions/component" + "github.com/openziti/fablab/kernel/lib/actions/host" + "github.com/openziti/fablab/kernel/lib/actions/semaphore" + "github.com/openziti/fablab/kernel/lib/binding" + "github.com/openziti/fablab/kernel/lib/runlevel/0_infrastructure/aws_ssh_key" + "github.com/openziti/fablab/kernel/lib/runlevel/0_infrastructure/semaphore" + "github.com/openziti/fablab/kernel/lib/runlevel/0_infrastructure/terraform" + distribution "github.com/openziti/fablab/kernel/lib/runlevel/3_distribution" + "github.com/openziti/fablab/kernel/lib/runlevel/3_distribution/rsync" + aws_ssh_key2 "github.com/openziti/fablab/kernel/lib/runlevel/6_disposal/aws_ssh_key" + "github.com/openziti/fablab/kernel/lib/runlevel/6_disposal/terraform" + "github.com/openziti/fablab/kernel/model" + "github.com/openziti/fablab/resources" + "github.com/openziti/foundation/v2/stringz" + "github.com/openziti/ziti/zititest/models/test_resources" + "github.com/openziti/ziti/zititest/zitilab" + "github.com/openziti/ziti/zititest/zitilab/actions/edge" + "github.com/openziti/ziti/zititest/zitilab/models" + "go.etcd.io/bbolt" + "os" + "path" + "strings" + "time" +) + +const ( + EnrollIdentitiesAction = "enrollIdentities" +) + +const ( + useLatestZitiOnRouters = true +) + +//go:embed configs +var configResource embed.FS + +var dbStrategyInstance = &dbStrategy{ + routerMappings: map[string]string{}, +} + +type dbStrategy struct { + routerMappings map[string]string +} + +func (self *dbStrategy) ProcessDbModel(tx *bbolt.Tx, m *model.Model, builder *models.ZitiDbBuilder) error { + dbFile := self.GetDbFile(m) + dbDir := path.Dir(dbFile) + mapFile := path.Join(dbDir, "edge-router-mapping.csv") + mapContents, err := os.ReadFile(mapFile) + if err != nil { + return fmt.Errorf("failed to read edge router mapping file (%w)", err) + } + + if err = self.loadErMap(string(mapContents)); err != nil { + return fmt.Errorf("unable to parse router mapping file (%w)", err) + } + + if err = self.ProcessEdgeRouters(tx, m, builder); err != nil { + return err + } + + if err = self.CreateEnrollIdentitiesAction(tx, m, builder); err != nil { + return err + } + + return nil +} + +func (self *dbStrategy) loadErMap(csv string) error { + sc := bufio.NewScanner(strings.NewReader(csv)) + for sc.Scan() { + parts := strings.Split(sc.Text(), ",") + if len(parts) >= 2 { + routerId := parts[0] + nfVersion := parts[1] + zitiVersion := nfZitiVersionMap[nfVersion] + self.routerMappings[routerId] = zitiVersion + } + } + return nil +} + +func (self *dbStrategy) GetDbFile(m *model.Model) string { + return m.MustStringVariable("db_file") +} + +func (self *dbStrategy) ProcessEdgeRouters(tx *bbolt.Tx, m *model.Model, builder *models.ZitiDbBuilder) error { + ids, _, err := builder.GetStores().EdgeRouter.QueryIds(tx, "true limit none") + if err != nil { + return err + } + + versions := map[string]int{} + hostsPerRegion := ((len(ids) / len(sites)) / 20) + 1 + + hostIdF := func(regionId string, idx int) string { + return fmt.Sprintf("%s-r%d", regionId[:len(regionId)-2], idx) + } + + echoServerIdF := func(regionId string, idx int) string { + return fmt.Sprintf("%s-echo%d", regionId[:len(regionId)-2], idx) + } + + simCount := 0 + + for _, site := range sites { + regionId := site[:len(site)-1] + region := m.Regions[regionId] + + if region != nil && region.Site != site { + return fmt.Errorf("trying to add region for site %v, but one exists, with different site", site) + } + + if region == nil { + region = &model.Region{ + Id: regionId, + Scope: model.Scope{Tags: model.Tags{}}, + Region: regionId, + Site: site, + Hosts: model.Hosts{}, + } + m.Regions[regionId] = region + } + + for i := 0; i < 10; i++ { + simCount++ + simId := fmt.Sprintf("sim%02d", simCount) + region.Hosts[simId] = &model.Host{ + InstanceType: "t3.small", + Components: model.Components{ + simId: { + Scope: model.Scope{Tags: model.Tags{"sim"}}, + Type: &zitilab.SimpleSimType{ + ConfigPath: "identities", + }, + }, + }, + } + } + + for i := 0; i < hostsPerRegion; i++ { + hostId := hostIdF(regionId, i) + routerHost := &model.Host{ + Id: hostId, + Scope: model.Scope{Tags: model.Tags{}}, + Region: region, + Components: model.Components{ + echoServerIdF(regionId, i): { + Scope: model.Scope{Tags: model.Tags{"server"}}, + Type: &zitilab.EchoServerType{ + Port: 8888, + }, + }, + }, + InstanceType: "t3.large", + ScaleIndex: uint32(i), + } + region.Hosts[hostId] = routerHost + } + } + + siteIdx := 0 + hostIdx := 0 + + for _, id := range ids { + er, err := builder.GetStores().EdgeRouter.LoadById(tx, id) + if err != nil { + return err + } + + if siteIdx >= len(sites) { + siteIdx = 0 + hostIdx++ + if hostIdx >= hostsPerRegion { + hostIdx = 0 + } + } + + site := sites[siteIdx] + regionId := site[:len(site)-1] + region := m.Regions[regionId] + hostId := hostIdF(regionId, hostIdx) + routerHost := region.Hosts[hostId] + + siteIdx++ + + version := "" + if !useLatestZitiOnRouters { + version = self.routerMappings[id] + if replacement, found := versionMapping[version]; found { + version = replacement + } + versions[version] = versions[version] + 1 + } + + routerComponent := &model.Component{ + Scope: model.Scope{Tags: model.Tags{"router", "pre-created"}}, + Type: &zitilab.RouterType{ + Version: version, + }, + Host: routerHost, + ScaleIndex: uint32(len(routerHost.Components)), + } + + routerHost.Components[er.Id] = routerComponent + + if er.IsTunnelerEnabled { + routerComponent.Scope.Tags = append(routerComponent.Scope.Tags, "tunneler") + } + + if stringz.Contains(er.RoleAttributes, "public") { + routerComponent.Scope.Tags = append(routerComponent.Scope.Tags, "public") + } + } + + return nil +} + +func (self *dbStrategy) CreateEnrollIdentitiesAction(tx *bbolt.Tx, m *model.Model, builder *models.ZitiDbBuilder) error { + ids, _, err := builder.GetStores().Identity.QueryIds(tx, `type != "Router" limit none`) + if err != nil { + return err + } + + m.Actions[EnrollIdentitiesAction] = func(m *model.Model) model.Action { + return model.ActionFunc(func(run model.Run) error { + return provisionIdentities(ids, run) + }) + } + + return nil +} + +var m = &model.Model{ + Id: "db-ctrl-test", + Scope: model.Scope{ + Defaults: model.Variables{ + "environment": "ctrl-test", + "credentials": model.Variables{ + "aws": model.Variables{ + "managed_key": true, + }, + "ssh": model.Variables{ + "username": "ubuntu", + }, + "edge": model.Variables{ + "username": "admin", + }, + }, + }, + }, + StructureFactories: []model.Factory{ + &models.ZitiDbBuilder{Strategy: dbStrategyInstance}, + }, + Resources: model.Resources{ + resources.Configs: resources.SubFolder(configResource, "configs"), + resources.Binaries: os.DirFS(path.Join(os.Getenv("GOPATH"), "bin")), + resources.Terraform: test_resources.TerraformResources(), + }, + Regions: model.Regions{ + "us-east-1": { + Region: "us-east-1", + Site: "us-east-1c", + Hosts: model.Hosts{ + "ctrl": { + InstanceType: "c5.9xlarge", + InstanceResourceType: "ondemand_iops", + EC2: model.EC2Host{ + Volume: model.EC2Volume{ + Type: "gp3", + SizeGB: 16, + IOPS: 3000, + }, + }, + Components: model.Components{ + "ctrl": { + Scope: model.Scope{Tags: model.Tags{"ctrl"}}, + Type: &zitilab.ControllerType{}, + }, + }, + }, + }, + }, + }, + + Actions: model.ActionBinders{ + "bootstrap": model.ActionBinder(func(m *model.Model) model.Action { + workflow := actions.Workflow() + + workflow.AddAction(component.StopInParallel("*", 200)) + workflow.AddAction(host.GroupExec("*", 25, "rm -f logs/*")) + + workflow.AddAction(component.Start("#ctrl")) + workflow.AddAction(semaphore.Sleep(2 * time.Second)) + + workflow.AddAction(edge.Login("#ctrl")) + + workflow.AddAction(model.ActionFunc(provisionRouters)) + workflow.AddAction(m.ExecuteAction("syncRouterJwts")) + workflow.AddAction(model.ActionFunc(enrollRouters)) + + workflow.AddAction(m.ExecuteAction(EnrollIdentitiesAction)) + workflow.AddAction(model.ActionFunc(func(run model.Run) error { + src := model.MakeBuildPath("identities") + return rsync.RsyncSelected("component.sim", src, "identities").Execute(run) + })) + return workflow + }), + "syncRouterJwts": model.Bind(model.ActionFunc(func(run model.Run) error { + src := model.MakeBuildPath("router-jwts") + return rsync.RsyncSelected("component.router", src, "router-jwts").Execute(run) + })), + "stop": model.Bind(component.StopInParallel("*", 200)), + "clean": model.Bind(actions.Workflow( + component.StopInParallel("*", 200), + host.GroupExec("*", 25, "rm -f logs/*"), + )), + "login": model.Bind(edge.Login("#ctrl")), + }, + + Infrastructure: model.Stages{ + aws_ssh_key.Express(), + terraform_0.Express(), + semaphore_0.Ready(90 * time.Second), + }, + + Distribution: model.Stages{ + distribution.DistributeSshKey("*"), + rsync.RsyncStaged(), + model.StageActionF(func(run model.Run) error { + quickRun, _ := run.GetModel().GetBoolVariable("quick_run") + _, targetedSync := run.GetModel().Scope.GetVariable("sync.target") + + if !quickRun && !targetedSync { + dbFile := dbStrategyInstance.GetDbFile(run.GetModel()) + deferred := rsync.NewRsyncHost("#ctrl", dbFile, "/home/ubuntu/ctrl.db") + return deferred.Execute(run) + } + return nil + }), + }, + + Disposal: model.Stages{ + terraform.Dispose(), + aws_ssh_key2.Dispose(), + }, +} + +func main() { + m.AddActivationActions("bootstrap") + + model.AddBootstrapExtension(binding.AwsCredentialsLoader) + model.AddBootstrapExtension(aws_ssh_key.KeyManager) + + fablab.InitModel(m) + fablab.Run() +} diff --git a/zititest/models/db-ctrl-test/model_constants.go b/zititest/models/db-ctrl-test/model_constants.go new file mode 100644 index 000000000..b4b903fa0 --- /dev/null +++ b/zititest/models/db-ctrl-test/model_constants.go @@ -0,0 +1,145 @@ +/* + Copyright 2019 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 main + +var versionMapping = map[string]string{ + "": "0.31.4", + "0.30.4": "0.30.5", + "0.31.0": "0.31.4", + "0.29.0": "0.28.1", + "0.25.2": "0.28.1", + "0.26.10": "0.28.1", +} + +var sites = []string{ + "us-east-1c", + "us-west-2b", + "eu-west-2a", + "eu-central-1a", + "ap-southeast-2a", +} + +var nfZitiVersionMap = map[string]string{ + "7.3.54": "0.24.3", + "7.3.77": "0.26.7", + "7.3.36": "0.20.13", + "7.3.45": "0.22.7", + "7.3.91": "0.28.1", + "7.3.58": "0.24.8", + "8.0.96": "0.31.0", + "7.3.32": "0.20.9", + "8.0.95": "0.30.5", + "7.3.92": "0.28.4", + "7.3.69": "0.25.13", + "7.3.57": "0.24.7", + "7.3.98": "0.31.4", + "7.3.71": "0.26.1", + "7.3.93": "0.29.0", + "7.3.22": "0.19.8", + "7.3.43": "0.22.4", + "7.3.31": "0.20.8", + "7.3.24": "0.19.10", + "7.3.87": "0.27.7", + "7.3.55": "0.24.4", + "7.3.86": "0.27.6", + "7.3.4": "0.17.5", + "7.3.25": "0.19.11", + "7.3.74": "0.26.4", + "7.3.81": "0.27.0", + "7.3.12": "0.18.9", + "8.0.98": "0.31.4", + "7.3.7": "0.18.3", + "7.3.51": "0.23.1", + "7.3.13": "0.18.10", + "7.3.5": "0.18.0", + "7.3.48": "0.22.10", + "7.3.30": "0.20.3", + "7.3.9": "0.18.5", + "7.3.94": "0.30.4", + "7.3.23": "0.19.9", + "7.3.89": "0.27.9", + "7.3.41": "0.22.2", + "8.0.94": "0.30.4", + "7.3.62": "0.25.2", + "7.3.19": "0.19.5", + "7.3.65": "0.25.5", + "7.3.67": "0.25.8", + "7.3.11": "0.18.8", + "7.3.73": "0.26.3", + "7.3.61": "0.24.13", + "7.3.14": "0.19.0", + "7.3.96": "0.31.0", + "7.3.82": "0.27.2", + "7.3.34": "0.20.11", + "7.3.78": "0.26.8", + "7.3.50": "0.23.0", + "7.3.90": "0.28.0", + "7.3.44": "0.22.6", + "7.3.56": "0.24.6", + "7.3.35": "0.20.12", + "7.3.64": "0.25.4", + "8.0.99": "0.32.2", + "7.3.49": "0.22.11", + "7.3.59": "0.24.9", + "7.3.88": "0.27.8", + "7.3.97": "0.31.2", + "7.3.52": "0.24.1", + "7.3.99": "0.32.2", + "7.3.29": "0.20.2", + "7.3.72": "0.26.2", + "7.3.37": "0.20.14", + "7.3.53": "0.24.2", + "7.3.10": "0.18.7", + "7.3.33": "0.20.10", + "7.3.26": "0.19.12", + "7.3.15": "0.19.1", + "7.3.38": "0.21.0", + "7.3.63": "0.25.3", + "7.3.46": "0.22.8", + "7.3.28": "0.20.1", + "7.3.75": "0.26.5", + "7.3.39": "0.22.0", + "8.0.100": "0.33.1", + "7.3.60": "0.24.12", + "7.3.95": "0.30.5", + "7.3.21": "0.19.7", + "8.0.85": "0.27.5", + "7.3.18": "0.19.4", + "7.3.6": "0.18.2", + "7.3.76": "0.26.6", + "7.3.42": "0.22.3", + "7.3.16": "0.19.2", + "7.3.20": "0.19.6", + "8.0.97": "0.31.2", + "7.3.70": "0.26.0", + "7.3.68": "0.25.10", + "7.3.66": "0.25.6", + "7.3.84": "0.27.4", + "7.3.8": "0.18.4", + "7.3.27": "0.19.13", + "8.0.93": "0.29.0", + "7.3.85": "0.27.5", + "7.3.17": "0.19.3", + "7.3.80": "0.26.11", + "7.3.100": "0.33.1", + "7.3.79": "0.26.10", + "7.3.40": "0.22.1", + "8.0.92": "0.28.4", + "7.3.83": "0.27.3", + "8.0.91": "0.28.1", + "7.3.47": "0.22.9", +} diff --git a/zititest/models/db-ctrl-test/provision.go b/zititest/models/db-ctrl-test/provision.go new file mode 100644 index 000000000..a564efac8 --- /dev/null +++ b/zititest/models/db-ctrl-test/provision.go @@ -0,0 +1,99 @@ +/* + Copyright 2019 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 main + +import ( + "fmt" + "github.com/openziti/fablab/kernel/lib/parallel" + "github.com/openziti/fablab/kernel/model" + "github.com/openziti/ziti/zititest/zitilab" + zitilib_actions "github.com/openziti/ziti/zititest/zitilab/actions" + "github.com/openziti/ziti/zititest/zitilab/cli" + "os" + "path/filepath" +) + +func provisionIdentities(identities []string, run model.Run) error { + var tasks []parallel.Task + + identitiesDir := model.MakeBuildPath("identities") + _ = os.MkdirAll(identitiesDir, 0770) + for _, v := range identities { + id := v + task := func() error { + jwtFileName := filepath.Join(identitiesDir, id+".jwt") + args := []string{"create", "enrollment", "ott", "--jwt-output-file", jwtFileName, "--", id} + + if err := zitilib_actions.EdgeExec(run.GetModel(), args...); err != nil { + return err + } + + if _, err := cli.Exec(m, "edge", "enroll", "--", jwtFileName); err != nil { + return fmt.Errorf("failed to enroll %s (%w)", id, err) + } + return nil + } + tasks = append(tasks, task) + } + + if err := parallel.Execute(tasks, 10); err != nil { + return err + } + + return nil +} + +func provisionRouters(run model.Run) error { + routerPkiDir := model.MakeBuildPath("router-jwts") + _ = os.MkdirAll(routerPkiDir, 0770) + + return run.GetModel().ForEachComponent(".router.pre-created", 15, func(c *model.Component) error { + jwtFileName := filepath.Join(routerPkiDir, c.Id+".jwt") + args := []string{"re-enroll", "edge-router", "-j", "--jwt-output-file", jwtFileName, "--", c.Id} + return zitilib_actions.EdgeExec(c.GetModel(), args...) + }) +} + +func enrollRouters(run model.Run) error { + zitiVersion := "" + ctrls := run.GetModel().SelectComponents(".ctrl") + for _, ctrl := range ctrls { + if ctrl.Type != nil { + zitiVersion = ctrl.Type.GetVersion() + break + } + } + + return run.GetModel().ForEachHost("component.router", 15, func(h *model.Host) error { + var cmds []string + cmds = append(cmds, "mkdir -p /home/ubuntu/router-pki/") + hostUser := h.GetSshUser() + for _, c := range h.Components { + var routerType *zitilab.RouterType + var ok bool + if routerType, ok = c.Type.(*zitilab.RouterType); !ok || !c.HasTag("pre-created") { + continue + } + + remoteJwt := fmt.Sprintf("/home/%s/router-jwts/%s.jwt", hostUser, c.Id) + tmpl := "set -o pipefail; %s router enroll /home/ubuntu/fablab/cfg/%s -j %s 2>&1 | tee /home/ubuntu/logs/%s.router.enroll.log " + cmd := fmt.Sprintf(tmpl, zitilab.GetZitiBinaryPath(c, zitiVersion), routerType.GetConfigName(c), remoteJwt, c.Id) + cmds = append(cmds, cmd) + } + return h.ExecLogOnlyOnError(cmds...) + }) +} diff --git a/zititest/models/db-sdk-hosting-test/main.go b/zititest/models/db-sdk-hosting-test/main.go index 9ff26d6a3..ff6ddfe1d 100644 --- a/zititest/models/db-sdk-hosting-test/main.go +++ b/zititest/models/db-sdk-hosting-test/main.go @@ -74,7 +74,7 @@ func (d dbStrategy) PostProcess(router *db.EdgeRouter, c *model.Component) { } func (d dbStrategy) ProcessDbModel(tx *bbolt.Tx, m *model.Model, builder *models.ZitiDbBuilder) error { - if err := builder.CreateEdgeRouterHosts(tx, m); err != nil { + if err := builder.CreateEdgeRouterHosts(tx, m, d); err != nil { return err } return d.CreateIdentityHosts(tx, m, builder) diff --git a/zititest/models/links-test/main.go b/zititest/models/links-test/main.go index 09d9207e0..653ce3869 100644 --- a/zititest/models/links-test/main.go +++ b/zititest/models/links-test/main.go @@ -100,7 +100,7 @@ var m = &model.Model{ }), model.FactoryFunc(func(m *model.Model) error { return m.ForEachHost("component.router", 1, func(host *model.Host) error { - host.InstanceType = "c5.xlarge" + host.InstanceType = "t3.large" return nil }) }), diff --git a/zititest/models/router-test/main.go b/zititest/models/router-test/main.go index d7f01a097..eed67d9d2 100644 --- a/zititest/models/router-test/main.go +++ b/zititest/models/router-test/main.go @@ -58,7 +58,7 @@ func (self scaleStrategy) GetEntityCount(entity model.Entity) uint32 { type dbStrategy struct{} func (d dbStrategy) ProcessDbModel(tx *bbolt.Tx, m *model.Model, builder *models.ZitiDbBuilder) error { - return builder.CreateEdgeRouterHosts(tx, m) + return builder.CreateEdgeRouterHosts(tx, m, d) } func (d dbStrategy) GetDbFile(*model.Model) string { diff --git a/zititest/models/smart/stages.go b/zititest/models/smart/stages.go index f0772641d..a061b2634 100644 --- a/zititest/models/smart/stages.go +++ b/zititest/models/smart/stages.go @@ -68,7 +68,7 @@ func (self *stageFactory) Build(m *model.Model) error { distribution.Locations(models.HasRouterComponent, "logs"), distribution.Locations(models.LoopListenerTag, "logs"), distribution.Locations(models.LoopDialerTag, "logs"), - rsync.Rsync(1), + rsync.RsyncStaged(), } // Run the bootstrap and start actions diff --git a/zititest/models/smoke/simple.go b/zititest/models/smoke/simple.go index 35ba09301..d6853c774 100644 --- a/zititest/models/smoke/simple.go +++ b/zititest/models/smoke/simple.go @@ -208,7 +208,9 @@ var Model = &model.Model{ }, "echo-server": { Scope: model.Scope{Tags: model.Tags{"sdk-app", "service"}}, - Type: &zitilab.EchoServerType{}, + Type: &zitilab.EchoServerType{ + BindService: "echo", + }, }, "iperf-server-ert": { Scope: model.Scope{Tags: model.Tags{"iperf", "service"}}, diff --git a/zititest/models/test_resources/terraform/main.tf b/zititest/models/test_resources/terraform/main.tf index b101aa159..efde51cad 100644 --- a/zititest/models/test_resources/terraform/main.tf +++ b/zititest/models/test_resources/terraform/main.tf @@ -34,6 +34,11 @@ module "{{ $regionId }}_host_{{ $hostId }}" { spot_price = "{{ $host.SpotPrice }}" spot_type = "{{ $host.SpotType }}" name = "{{ $region.Model.MustVariable "environment" }}.{{ $host.Id }}" + {{ if not (eq $host.EC2.Volume.Type "") }} + volume_type = "{{ $host.EC2.Volume.Type }}" + volume_size = "{{ $host.EC2.Volume.SizeGB }}" + volume_iops = "{{ $host.EC2.Volume.IOPS }}" + {{ end }} } output "{{ $regionId }}_host_{{ $hostId }}_public_ip" { value = module.{{ $regionId }}_host_{{ $hostId }}.public_ip } diff --git a/zititest/models/test_resources/terraform/ondemand_iops_instance/main.tf b/zititest/models/test_resources/terraform/ondemand_iops_instance/main.tf new file mode 100644 index 000000000..e90664e41 --- /dev/null +++ b/zititest/models/test_resources/terraform/ondemand_iops_instance/main.tf @@ -0,0 +1,58 @@ +variable "name" {} +variable "access_key" {} +variable "secret_key" {} +variable "environment_tag" { default = "" } +variable "instance_type" {} +variable "key_name" {} +variable "key_path" {} +variable "region" {} +variable "security_group_id" {} +variable "ssh_user" { default = "ubuntu" } +variable "subnet_id" {} +variable "spot_price" {} +variable "spot_type" {} +variable "volume_type" { default = "gp2" } +variable "volume_size" { default = 4 } +variable "volume_iops" {} + +output "public_ip" { value = aws_instance.fablab.public_ip } +output "private_ip" { value = aws_instance.fablab.private_ip } + +provider "aws" { + access_key = var.access_key + secret_key = var.secret_key + region = var.region +} + +data "aws_ami" "ami" { + most_recent = true + owners = ["self"] + + filter { + name = "name" + values = ["ziti-tests-*"] + } +} + +resource "aws_instance" "fablab" { + ami = data.aws_ami.ami.id + instance_type = var.instance_type + key_name = var.key_name + vpc_security_group_ids = [var.security_group_id] + subnet_id = var.subnet_id + associate_public_ip_address = true + + root_block_device { + volume_type = var.volume_type + volume_size = var.volume_size + iops = var.volume_iops + } + + tags = { + Name = var.name + cost_environment = "non-production" + cost_category = "CICD" + cost_team_owner = "ADVDEV" + source = "fablab" + } +} diff --git a/zititest/simple-sim/simple-sim.go b/zititest/simple-sim/simple-sim.go new file mode 100644 index 000000000..b334cd805 --- /dev/null +++ b/zititest/simple-sim/simple-sim.go @@ -0,0 +1,215 @@ +/* + 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 main + +import ( + "fmt" + "github.com/michaelquigley/pfxlog" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/sdk-golang/ziti" + "github.com/openziti/ziti/common/eid" + cmap "github.com/orcaman/concurrent-map/v2" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "math/rand" + "os" + "path/filepath" + "slices" + "time" +) + +type simpleSimAction struct { + verbose bool + logFormatter string + configFile string + + authenticated cmap.ConcurrentMap[string, time.Time] +} + +func newSimpleSimCmd() *cobra.Command { + action := &simpleSimAction{ + authenticated: cmap.New[time.Time](), + } + + cmd := &cobra.Command{ + Use: "simple-sim ", + Short: "Generates traffic to one more ziti identities", + Args: cobra.ExactArgs(1), + Run: action.run, + } + + // allow interspersing positional args and flags + cmd.Flags().SetInterspersed(true) + cmd.Flags().BoolVarP(&action.verbose, "verbose", "v", false, "Enable verbose logging") + cmd.Flags().StringVar(&action.logFormatter, "log-formatter", "", "Specify log formatter [json|pfxlog|text]") + cmd.Flags().StringVarP(&action.configFile, "identity", "i", "", "Specify the Ziti identity to use. If not specified the Ziti listener won't be started") + cmd.Flags().SetInterspersed(true) + + return cmd +} + +func (self *simpleSimAction) initLogging() { + logLevel := logrus.InfoLevel + if self.verbose { + logLevel = logrus.DebugLevel + } + + options := pfxlog.DefaultOptions().SetTrimPrefix("github.com/openziti/").NoColor() + pfxlog.GlobalInit(logLevel, options) + + switch self.logFormatter { + case "pfxlog": + pfxlog.SetFormatter(pfxlog.NewFormatter(pfxlog.DefaultOptions().SetTrimPrefix("github.com/openziti/").StartingToday())) + case "json": + pfxlog.SetFormatter(&logrus.JSONFormatter{TimestampFormat: "2006-01-02T15:04:05.000Z"}) + case "text": + pfxlog.SetFormatter(&logrus.TextFormatter{}) + default: + // let logrus do its own thing + } +} + +func (self *simpleSimAction) run(_ *cobra.Command, args []string) { + self.initLogging() + + log := pfxlog.Logger() + + fsInfo, err := os.Stat(args[0]) + if err != nil { + log.WithError(err).Fatalf("unable to stat config file/dir %s", args[0]) + return + } + + simCount := 0 + if fsInfo.IsDir() { + files, err := os.ReadDir(args[0]) + if err != nil { + log.WithError(err).Fatalf("failed to scan directory %s:", args[0]) + return + } + + for _, file := range files { + if filepath.Ext(file.Name()) == ".json" { + fn, err := filepath.Abs(filepath.Join(args[0], file.Name())) + if err != nil { + log.Fatalf("failed to listing file %s: %v", file.Name(), err) + } + self.startSimForIdentity(fn) + simCount++ + } + } + } + + for { + time.Sleep(time.Second * 5) + pfxlog.Logger().Infof("%d/%d identities authenticated", self.authenticated.Count(), simCount) + } +} + +func (self *simpleSimAction) startSimForIdentity(path string) { + log := pfxlog.Logger().WithField("config", path) + + zitiConfig, err := ziti.NewConfigFromFile(path) + if err != nil { + log.WithError(err).Fatal("unable to load ziti identity") + return + } + + zitiContext, err := ziti.NewContext(zitiConfig) + if err != nil { + log.WithError(err).Fatal("could not create sdk context from config") + } + + go self.runSim(path, zitiContext) +} + +func (self *simpleSimAction) runSim(path string, ctx ziti.Context) { + log := pfxlog.Logger().WithField("config", path) + + var lastLog time.Time + cycle := 0 + + for { + cycle++ + validServices := 0 + dialSuccesses := 0 + services, err := ctx.GetServices() + if err != nil { + self.authenticated.Remove(ctx.GetId()) + log.WithField("cycle", cycle).WithError(err).Error("unable to list services") + time.Sleep(time.Second * 5) + continue + } else { + self.authenticated.Set(ctx.GetId(), time.Now()) + } + + for _, service := range services { + if !slices.Contains(service.Permissions, rest_model.DialBindDial) { + continue + } + validServices++ + + svcLog := log.WithField("svc", *service.Name).WithField("cycle", cycle) + conn, err := ctx.Dial(*service.Name) + if err != nil { + svcLog.WithError(err).Error("unable to dial service") + } else { + randomString := eid.New() + if _, err = conn.Write([]byte(randomString)); err != nil { + svcLog.WithError(err).Error("unable to write") + } else { + result := make([]byte, len(randomString)) + n, err := conn.Read(result) + if err != nil { + svcLog.WithError(err).Error("unable to read") + } else { + result = result[:n] + if string(result) != randomString { + svcLog.WithError(err).Error("%s != %s", randomString, result) + } else { + dialSuccesses++ + } + } + } + } + + if conn != nil { + if err = conn.Close(); err != nil { + svcLog.WithError(err).Error("error closing conn") + } + time.Sleep(time.Millisecond * 250) + } + } + + if time.Since(lastLog) > time.Minute { + log.WithField("serviceCount", len(services)). + WithField("cycle", cycle). + WithField("successfulDials", fmt.Sprintf("%d/%d", dialSuccesses, validServices)). + Info("completed") + lastLog = time.Now() + } + + time.Sleep(time.Duration(500+rand.Intn(1000)) * time.Second) + } +} + +func main() { + cli := newSimpleSimCmd() + if err := cli.Execute(); err != nil { + pfxlog.Logger().Fatal(err.Error()) + } +} diff --git a/zititest/ziti-fabric-test/main.go b/zititest/ziti-fabric-test/main.go index c4b1ecefa..34179bc20 100644 --- a/zititest/ziti-fabric-test/main.go +++ b/zititest/ziti-fabric-test/main.go @@ -19,8 +19,6 @@ package main import ( - "math/rand" - "github.com/michaelquigley/pfxlog" "github.com/openziti/transport/v2" "github.com/openziti/transport/v2/tcp" @@ -28,7 +26,6 @@ import ( "github.com/openziti/transport/v2/transwarp" "github.com/openziti/transport/v2/transwarptls" "github.com/openziti/transport/v2/wss" - "github.com/openziti/foundation/v2/info" "github.com/openziti/ziti/zititest/ziti-fabric-test/subcmd" _ "github.com/openziti/ziti/zititest/ziti-fabric-test/subcmd/client" _ "github.com/openziti/ziti/zititest/ziti-fabric-test/subcmd/loop2" @@ -46,6 +43,5 @@ func init() { } func main() { - rand.Seed(info.NowInMilliseconds()) subcmd.Execute() } diff --git a/zititest/zitilab/cli/cli.go b/zititest/zitilab/cli/cli.go index dfd4b1e4f..4b243c7b5 100644 --- a/zititest/zitilab/cli/cli.go +++ b/zititest/zitilab/cli/cli.go @@ -39,7 +39,7 @@ func Exec(m *model.Model, args ...string) (string, error) { ziticli.SetArgs(args) logrus.Infof("executing: %s", strings.Join(args, " ")) if err := ziticli.Execute(); err != nil { - logrus.Errorf("err executing command, err:[%e]", err) + logrus.WithError(err).Error("err executing command") } return cliOut.String(), nil diff --git a/zititest/zitilab/component_common.go b/zititest/zitilab/component_common.go index 31d23922a..6174f7929 100644 --- a/zititest/zitilab/component_common.go +++ b/zititest/zitilab/component_common.go @@ -38,7 +38,7 @@ func getZitiProcessFilter(c *model.Component, zitiType string) func(string) bool func startZitiComponent(c *model.Component, zitiType string, version string, configName string) error { user := c.GetHost().GetSshUser() - binaryPath := getZitiBinaryPath(c, version) + binaryPath := GetZitiBinaryPath(c, version) configPath := fmt.Sprintf("/home/%s/fablab/cfg/%s", user, configName) logsPath := fmt.Sprintf("/home/%s/logs/%s.log", user, c.Id) @@ -74,7 +74,7 @@ func canonicalizeGoAppVersion(version *string) { } } -func getZitiBinaryPath(c *model.Component, version string) string { +func GetZitiBinaryPath(c *model.Component, version string) string { return getBinaryPath(c, "ziti", version) } @@ -114,3 +114,19 @@ func reEnrollIdentity(run model.Run, c *model.Component, zitiBinaryPath string, return c.GetHost().ExecLogOnlyOnError(cmd) } + +func setupDnsForTunneler(c *model.Component) error { + key := "ziti_tunnel.resolve_setup_done" + if _, found := c.Host.Data[key]; !found { + cmds := []string{ + "sudo sed -i 's/#DNS=/DNS=127.0.0.1/g' /etc/systemd/resolved.conf", + "sudo systemctl restart systemd-resolved", + } + if err := c.Host.ExecLogOnlyOnError(cmds...); err != nil { + return err + } + c.Host.Data[key] = true + return nil + } + return nil +} diff --git a/zititest/zitilab/component_controller.go b/zititest/zitilab/component_controller.go index d7ded6f76..d641318f7 100644 --- a/zititest/zitilab/component_controller.go +++ b/zititest/zitilab/component_controller.go @@ -45,6 +45,14 @@ type ControllerType struct { DNSNames []string } +func (self *ControllerType) Label() string { + return "ziti-controller" +} + +func (self *ControllerType) GetVersion() string { + return self.Version +} + func (self *ControllerType) InitType(*model.Component) { canonicalizeGoAppVersion(&self.Version) } @@ -104,7 +112,15 @@ func (self *ControllerType) IsRunning(_ model.Run, c *model.Component) (bool, er return len(pids) > 0, nil } -func (self *ControllerType) Start(_ model.Run, c *model.Component) error { +func (self *ControllerType) Start(r model.Run, c *model.Component) error { + isRunninng, err := self.IsRunning(r, c) + if err != nil { + return err + } + if isRunninng { + fmt.Printf("controller %s already started\n", c.Id) + return nil + } return startZitiComponent(c, "controller", self.Version, self.getConfigName(c)) } @@ -113,7 +129,7 @@ func (self *ControllerType) Stop(_ model.Run, c *model.Component) error { } func (self *ControllerType) GetBinaryPath(c *model.Component) string { - return getZitiBinaryPath(c, self.Version) + return GetZitiBinaryPath(c, self.Version) } func (self *ControllerType) InitStandalone(run model.Run, c *model.Component) error { @@ -130,7 +146,7 @@ func (self *ControllerType) InitStandalone(run model.Run, c *model.Component) er factory := c.GetHost().NewSshConfigFactory() - binaryPath := getZitiBinaryPath(c, self.Version) + binaryPath := GetZitiBinaryPath(c, self.Version) configPath := fmt.Sprintf("/home/%s/fablab/cfg/%s", factory.User(), self.getConfigName(c)) tmpl := "rm -f /home/%v/fablab/ctrl.db && set -o pipefail; %s controller --log-formatter pfxlog edge init %s -u %s -p %s 2>&1 | tee logs/controller.edge.init.log" @@ -142,7 +158,7 @@ func (self *ControllerType) InitRaft(run model.Run, c *model.Component) error { count := 0 err := run.GetModel().ForEachComponent("*", 1, func(other *model.Component) error { if _, ok := c.Type.(*ControllerType); ok { - binaryPath := getZitiBinaryPath(c, self.Version) + binaryPath := GetZitiBinaryPath(c, self.Version) tmpl := "%s agent cluster add %v --id %v" cmd := fmt.Sprintf(tmpl, binaryPath, "tls:"+other.Host.PublicIp+":6262", other.Id) count++ diff --git a/zititest/zitilab/component_echo_server.go b/zititest/zitilab/component_echo_server.go index 6ea0e7f98..ba9279e5b 100644 --- a/zititest/zitilab/component_echo_server.go +++ b/zititest/zitilab/component_echo_server.go @@ -1,3 +1,19 @@ +/* + Copyright 2019 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 zitilab import ( @@ -13,8 +29,18 @@ var _ model.ComponentType = (*EchoServerType)(nil) type EchoServerMode int type EchoServerType struct { - Version string - LocalPath string + Version string + LocalPath string + Port uint16 + BindService string +} + +func (self *EchoServerType) Label() string { + return "echo-server" +} + +func (self *EchoServerType) GetVersion() string { + return self.Version } func (self *EchoServerType) InitType(*model.Component) { @@ -48,7 +74,7 @@ func (self *EchoServerType) IsRunning(_ model.Run, c *model.Component) (bool, er func (self *EchoServerType) Start(run model.Run, c *model.Component) error { user := c.GetHost().GetSshUser() - binaryPath := getZitiBinaryPath(c, self.Version) + binaryPath := GetZitiBinaryPath(c, self.Version) configPath := fmt.Sprintf("/home/%s/fablab/cfg/%s.json", user, c.Id) logsPath := fmt.Sprintf("/home/%s/logs/%s.log", user, c.Id) @@ -57,8 +83,18 @@ func (self *EchoServerType) Start(run model.Run, c *model.Component) error { ha = "--ha" } - serviceCmd := fmt.Sprintf("nohup %s demo echo-server --cli-agent-alias %s %s -i %s > %s 2>&1 &", - binaryPath, c.Id, ha, configPath, logsPath) + serviceHostingFlags := "" + if self.BindService != "" { + serviceHostingFlags = fmt.Sprintf("-i %s -s %s", configPath, self.BindService) + } + + portFlag := "" + if self.Port > 0 { + portFlag = fmt.Sprintf("-p %d", self.Port) + } + + serviceCmd := fmt.Sprintf("nohup %s demo echo-server --cli-agent-alias %s %s %s %s > %s 2>&1 &", + binaryPath, c.Id, ha, serviceHostingFlags, portFlag, logsPath) value, err := c.GetHost().ExecLogged(serviceCmd) if err != nil { diff --git a/zititest/zitilab/component_iperf.go b/zititest/zitilab/component_iperf.go index eedac0073..738b1f16d 100644 --- a/zititest/zitilab/component_iperf.go +++ b/zititest/zitilab/component_iperf.go @@ -29,6 +29,14 @@ type IPerfServerType struct { Port uint16 } +func (self *IPerfServerType) Label() string { + return "iperf-server" +} + +func (self *IPerfServerType) GetVersion() string { + return "os-provided" +} + func (self *IPerfServerType) Dump() any { return map[string]string{ "type_id": "iperf-server", diff --git a/zititest/zitilab/component_router.go b/zititest/zitilab/component_router.go index 23dd92de3..89123e0e9 100644 --- a/zititest/zitilab/component_router.go +++ b/zititest/zitilab/component_router.go @@ -19,7 +19,6 @@ package zitilab import ( "fmt" "github.com/openziti/fablab/kernel/lib" - "github.com/openziti/fablab/kernel/lib/actions/host" "github.com/openziti/fablab/kernel/model" zitilib_actions "github.com/openziti/ziti/zititest/zitilab/actions" "github.com/openziti/ziti/zititest/zitilab/stageziti" @@ -45,6 +44,14 @@ type RouterType struct { LocalPath string } +func (self *RouterType) Label() string { + return "ziti-router" +} + +func (self *RouterType) GetVersion() string { + return self.Version +} + func (self *RouterType) InitType(*model.Component) { canonicalizeGoAppVersion(&self.Version) } @@ -68,12 +75,7 @@ func (self *RouterType) Dump() any { func (self *RouterType) InitializeHost(run model.Run, c *model.Component) error { if self.isTunneler(c) { - cmds := []string{ - "sudo sed -i 's/#DNS=/DNS=127.0.0.1/g' /etc/systemd/resolved.conf", - "sudo systemctl restart systemd-resolved", - "mkdir -p /home/ubuntu/logs", - } - return host.Exec(c.GetHost(), cmds...).Execute(run) + return setupDnsForTunneler(c) } return nil } @@ -84,7 +86,7 @@ func (self *RouterType) StageFiles(r model.Run, c *model.Component) error { configSource = "router.yml.tmpl" } - configName := self.getConfigName(c) + configName := self.GetConfigName(c) if err := lib.GenerateConfigForComponent(c, self.ConfigSourceFS, configSource, configName, r); err != nil { return err @@ -97,7 +99,7 @@ func (self *RouterType) isTunneler(c *model.Component) bool { return c.HasLocalOrAncestralTag("tunneler") } -func (self *RouterType) getConfigName(c *model.Component) string { +func (self *RouterType) GetConfigName(c *model.Component) string { configName := self.ConfigName if configName == "" { configName = c.Id + ".yml" @@ -126,7 +128,7 @@ func (self *RouterType) Start(r model.Run, c *model.Component) error { fmt.Printf("router %s already started\n", c.Id) return nil } - return startZitiComponent(c, "router", self.Version, self.getConfigName(c)) + return startZitiComponent(c, "router", self.Version, self.GetConfigName(c)) } func (self *RouterType) Stop(run model.Run, c *model.Component) error { @@ -174,15 +176,15 @@ func (self *RouterType) CreateAndEnroll(run model.Run, c *model.Component) error } tmpl := "set -o pipefail; %s router enroll /home/ubuntu/fablab/cfg/%s -j %s 2>&1 | tee /home/ubuntu/logs/%s.router.enroll.log " - cmd := fmt.Sprintf(tmpl, getZitiBinaryPath(c, self.Version), self.getConfigName(c), remoteJwt, c.Id) + cmd := fmt.Sprintf(tmpl, GetZitiBinaryPath(c, self.Version), self.GetConfigName(c), remoteJwt, c.Id) return c.GetHost().ExecLogOnlyOnError(cmd) } -func (self *RouterType) ReEnroll(_ model.Run, c *model.Component) error { +func (self *RouterType) ReEnroll(run model.Run, c *model.Component) error { jwtFileName := filepath.Join(model.ConfigBuild(), c.Id+".jwt") - args := []string{"re-enroll", "edge-router", c.Id, "-j", "--jwt-output-file", jwtFileName} + args := []string{"re-enroll", "edge-router", "-j", "--jwt-output-file", jwtFileName, "--", c.Id} if err := zitilib_actions.EdgeExec(c.GetModel(), args...); err != nil { return err @@ -193,8 +195,18 @@ func (self *RouterType) ReEnroll(_ model.Run, c *model.Component) error { return err } + zitiVersion := self.Version + + ctrls := run.GetModel().SelectComponents(".ctrl") + for _, ctrl := range ctrls { + if ctrl.Type != nil { + zitiVersion = ctrl.Type.GetVersion() + break + } + } + tmpl := "set -o pipefail; %s router enroll /home/ubuntu/fablab/cfg/%s -j %s 2>&1 | tee /home/ubuntu/logs/%s.router.enroll.log " - cmd := fmt.Sprintf(tmpl, getZitiBinaryPath(c, self.Version), self.getConfigName(c), remoteJwt, c.Id) + cmd := fmt.Sprintf(tmpl, GetZitiBinaryPath(c, zitiVersion), self.GetConfigName(c), remoteJwt, c.Id) return c.GetHost().ExecLogOnlyOnError(cmd) } diff --git a/zititest/zitilab/component_simple_sim.go b/zititest/zitilab/component_simple_sim.go new file mode 100644 index 000000000..319aad6a4 --- /dev/null +++ b/zititest/zitilab/component_simple_sim.go @@ -0,0 +1,94 @@ +/* + Copyright 2019 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 zitilab + +import ( + "fmt" + "github.com/openziti/fablab/kernel/model" + "github.com/openziti/ziti/zititest/zitilab/stageziti" + "github.com/sirupsen/logrus" + "strings" +) + +var _ model.ComponentType = (*SimpleSimType)(nil) + +type SimpleSimType struct { + LocalPath string + ConfigPath string +} + +func (self *SimpleSimType) Label() string { + return "simple-sim" +} + +func (self *SimpleSimType) GetVersion() string { + return "local" +} + +func (self *SimpleSimType) Dump() any { + return map[string]string{ + "type_id": "simple-sim", + "local_path": self.LocalPath, + "config_path": self.ConfigPath, + } +} + +func (self *SimpleSimType) StageFiles(r model.Run, c *model.Component) error { + return stageziti.StageLocalOnce(r, "simple-sim", c, self.LocalPath) +} + +func (self *SimpleSimType) getProcessFilter() func(string) bool { + return func(s string) bool { + return strings.Contains(s, "simple-sim") + } +} + +func (self *SimpleSimType) IsRunning(_ model.Run, c *model.Component) (bool, error) { + pids, err := c.GetHost().FindProcesses(self.getProcessFilter()) + if err != nil { + return false, err + } + return len(pids) > 0, nil +} + +func (self *SimpleSimType) Start(_ model.Run, c *model.Component) error { + user := c.GetHost().GetSshUser() + + binaryPath := getBinaryPath(c, "simple-sim", "") + logsPath := fmt.Sprintf("/home/%s/logs/%s.log", user, c.Id) + configPath := fmt.Sprintf("/home/%s/%s", user, self.ConfigPath) + + serviceCmd := fmt.Sprintf("%s %s --log-formatter pfxlog > %s 2>&1 &", + binaryPath, configPath, logsPath) + + value, err := c.Host.ExecLogged( + "rm -f "+logsPath, + serviceCmd) + if err != nil { + return err + } + + if len(value) > 0 { + logrus.Infof("output [%s]", strings.Trim(value, " \t\r\n")) + } + + return nil +} + +func (self *SimpleSimType) Stop(_ model.Run, c *model.Component) error { + return c.GetHost().KillProcesses("-TERM", self.getProcessFilter()) +} diff --git a/zititest/zitilab/component_zcat.go b/zititest/zitilab/component_zcat.go index 162867850..a4aeefc59 100644 --- a/zititest/zitilab/component_zcat.go +++ b/zititest/zitilab/component_zcat.go @@ -1,3 +1,19 @@ +/* + Copyright 2019 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 zitilab import ( @@ -15,6 +31,14 @@ type ZCatType struct { LocalPath string } +func (self *ZCatType) Label() string { + return "zcat" +} + +func (self *ZCatType) GetVersion() string { + return self.Version +} + func (self *ZCatType) InitType(*model.Component) { canonicalizeGoAppVersion(&self.Version) } diff --git a/zititest/zitilab/component_ziti_edge_tunnel.go b/zititest/zitilab/component_ziti_edge_tunnel.go index a6fd4e147..67d3c8a6f 100644 --- a/zititest/zitilab/component_ziti_edge_tunnel.go +++ b/zititest/zitilab/component_ziti_edge_tunnel.go @@ -34,6 +34,14 @@ type ZitiEdgeTunnelType struct { ConfigPathF func(c *model.Component) string } +func (self *ZitiEdgeTunnelType) Label() string { + return "ziti-edge-tunnel" +} + +func (self *ZitiEdgeTunnelType) GetVersion() string { + return self.Version +} + func (self *ZitiEdgeTunnelType) GetActions() map[string]model.ComponentAction { return map[string]model.ComponentAction{ ZitiTunnelActionsReEnroll: model.ComponentActionF(self.ReEnroll), @@ -125,5 +133,5 @@ func (self *ZitiEdgeTunnelType) Stop(_ model.Run, c *model.Component) error { } func (self *ZitiEdgeTunnelType) ReEnroll(run model.Run, c *model.Component) error { - return reEnrollIdentity(run, c, getZitiBinaryPath(c, self.ZitiVersion), self.GetConfigPath(c)) + return reEnrollIdentity(run, c, GetZitiBinaryPath(c, self.ZitiVersion), self.GetConfigPath(c)) } diff --git a/zititest/zitilab/component_ziti_tunnel.go b/zititest/zitilab/component_ziti_tunnel.go index 9ddab77ac..e8b3d6d05 100644 --- a/zititest/zitilab/component_ziti_tunnel.go +++ b/zititest/zitilab/component_ziti_tunnel.go @@ -56,6 +56,14 @@ type ZitiTunnelType struct { ConfigPathF func(c *model.Component) string } +func (self *ZitiTunnelType) Label() string { + return "ziti-tunnel" +} + +func (self *ZitiTunnelType) GetVersion() string { + return self.Version +} + func (self *ZitiTunnelType) GetActions() map[string]model.ComponentAction { return map[string]model.ComponentAction{ ZitiTunnelActionsReEnroll: model.ComponentActionF(self.ReEnroll), @@ -80,18 +88,7 @@ func (self *ZitiTunnelType) StageFiles(r model.Run, c *model.Component) error { func (self *ZitiTunnelType) InitializeHost(_ model.Run, c *model.Component) error { if self.Mode == ZitiTunnelModeTproxy { - key := "ziti_tunnel.resolve_setup_done" - if _, found := c.Host.Data[key]; !found { - cmds := []string{ - "sudo sed -i 's/#DNS=/DNS=127.0.0.1/g' /etc/systemd/resolved.conf", - "sudo systemctl restart systemd-resolved", - } - if err := c.Host.ExecLogOnlyOnError(cmds...); err != nil { - return err - } - c.Host.Data[key] = true - return nil - } + return setupDnsForTunneler(c) } return nil } @@ -120,7 +117,7 @@ func (self *ZitiTunnelType) Start(_ model.Run, c *model.Component) error { user := c.GetHost().GetSshUser() - binaryPath := getZitiBinaryPath(c, self.Version) + binaryPath := GetZitiBinaryPath(c, self.Version) configPath := self.GetConfigPath(c) logsPath := fmt.Sprintf("/home/%s/logs/%s.log", user, c.Id) @@ -151,5 +148,5 @@ func (self *ZitiTunnelType) Stop(_ model.Run, c *model.Component) error { } func (self *ZitiTunnelType) ReEnroll(run model.Run, c *model.Component) error { - return reEnrollIdentity(run, c, getZitiBinaryPath(c, self.Version), self.GetConfigPath(c)) + return reEnrollIdentity(run, c, GetZitiBinaryPath(c, self.Version), self.GetConfigPath(c)) } diff --git a/zititest/zitilab/component_zrok_controller.go b/zititest/zitilab/component_zrok_controller.go index 42a49797c..aa60c6dcb 100644 --- a/zititest/zitilab/component_zrok_controller.go +++ b/zititest/zitilab/component_zrok_controller.go @@ -47,6 +47,14 @@ type ZrokControllerType struct { PreCreateClients string } +func (self *ZrokControllerType) Label() string { + return "zrok-controller" +} + +func (self *ZrokControllerType) GetVersion() string { + return self.Version +} + func (self *ZrokControllerType) InitType(*model.Component) { canonicalizeGoAppVersion(&self.Version) } diff --git a/zititest/zitilab/component_zrok_frontend.go b/zititest/zitilab/component_zrok_frontend.go index e11412042..040825e8b 100644 --- a/zititest/zitilab/component_zrok_frontend.go +++ b/zititest/zitilab/component_zrok_frontend.go @@ -46,6 +46,14 @@ type ZrokFrontendType struct { ZrokCtrlSelector string } +func (self *ZrokFrontendType) Label() string { + return "zrok-frontend" +} + +func (self *ZrokFrontendType) GetVersion() string { + return self.Version +} + func (self *ZrokFrontendType) InitType(*model.Component) { canonicalizeGoAppVersion(&self.Version) if self.ZrokCtrlSelector == "" { diff --git a/zititest/zitilab/component_zrok_looptest.go b/zititest/zitilab/component_zrok_looptest.go index 26c6b971e..9939486ac 100644 --- a/zititest/zitilab/component_zrok_looptest.go +++ b/zititest/zitilab/component_zrok_looptest.go @@ -39,6 +39,14 @@ type ZrokLoopTestType struct { Pacing time.Duration } +func (self *ZrokLoopTestType) Label() string { + return "zrok-loop-test" +} + +func (self *ZrokLoopTestType) GetVersion() string { + return self.Version +} + func (self *ZrokLoopTestType) InitType(*model.Component) { canonicalizeGoAppVersion(&self.Version) if self.Iterations == 0 { diff --git a/zititest/zitilab/models/db_builder.go b/zititest/zitilab/models/db_builder.go index e02efab21..d8c3ca858 100644 --- a/zititest/zitilab/models/db_builder.go +++ b/zititest/zitilab/models/db_builder.go @@ -15,9 +15,12 @@ import ( type ZitiDbBuilderStrategy interface { GetDbFile(m *model.Model) string + ProcessDbModel(tx *bbolt.Tx, m *model.Model, builder *ZitiDbBuilder) error +} + +type ZitiEdgeRouterStrategy interface { GetSite(router *db.EdgeRouter) (string, bool) PostProcess(router *db.EdgeRouter, c *model.Component) - ProcessDbModel(tx *bbolt.Tx, m *model.Model, builder *ZitiDbBuilder) error } type ZitiDbBuilder struct { @@ -63,7 +66,7 @@ func (self *ZitiDbBuilder) Build(m *model.Model) error { }) } -func (self *ZitiDbBuilder) CreateEdgeRouterHosts(tx *bbolt.Tx, m *model.Model) error { +func (self *ZitiDbBuilder) CreateEdgeRouterHosts(tx *bbolt.Tx, m *model.Model, erStrategy ZitiEdgeRouterStrategy) error { ids, _, err := self.stores.EdgeRouter.QueryIds(tx, "true limit none") if err != nil { return err @@ -75,7 +78,7 @@ func (self *ZitiDbBuilder) CreateEdgeRouterHosts(tx *bbolt.Tx, m *model.Model) e return err } - if site, useEdgeRouter := self.Strategy.GetSite(er); useEdgeRouter { + if site, useEdgeRouter := erStrategy.GetSite(er); useEdgeRouter { regionId := site[:len(site)-1] var region *model.Region @@ -114,7 +117,7 @@ func (self *ZitiDbBuilder) CreateEdgeRouterHosts(tx *bbolt.Tx, m *model.Model) e } host.Components[er.Id] = component - self.Strategy.PostProcess(er, component) + erStrategy.PostProcess(er, component) } } return nil diff --git a/zititest/zitilab/stageziti/stageziti.go b/zititest/zitilab/stageziti/stageziti.go index 325d86b7a..a0486239f 100644 --- a/zititest/zitilab/stageziti/stageziti.go +++ b/zititest/zitilab/stageziti/stageziti.go @@ -64,6 +64,15 @@ func StageZrok(run model.Run, component *model.Component, version string, source }) } +func StageLocalOnce(run model.Run, executable string, component *model.Component, source string) error { + op := fmt.Sprintf("install.%s-local", executable) + return run.DoOnce(op, func() error { + return StageExecutable(run, executable, component, "", source, func() error { + return fmt.Errorf("unable to fetch %s, as it a local-only application", executable) + }) + }) +} + func StageExecutable(run model.Run, executable string, component *model.Component, version string, source string, fallbackF func() error) error { fileName := executable if version != "" {