Merge branch 'release-next' into ziti-tunnel-docker

This commit is contained in:
Ken Bingham
2021-04-15 16:32:26 -04:00
committed by GitHub
14 changed files with 107 additions and 263 deletions
+30
View File
@@ -1,3 +1,33 @@
# Release 0.19.11
## What's New
* Add workaround for bbolt bug which caused some data to get left behind when deleting identities, seen when turning off tunneler capability on edge routers
* Remove deprecated ziti-enroller command. Enrollement can be done using the ziti-tunnel, ziti-router and ziti commands
* Fix UDP intercept handling
* The host.v1 service configuration type has been changed as follows:
* Rename `dialIntercepted*` properties to `forwardProtocol`, `forwardAddress`, `forwardPort` for better consistency with non-tunneler client applications.
* Add `allowedProtocols`, `allowedAddresses`, and `allowedPortRanges` properties to whitelist destinations that are dialed via `forward*`. The `allowed*` properties are required for any corresponding `forward*` property that is `true`.
* Add `allowedSourceAddresses`, which serves as a whitelist for source IPs/CIDRs and informs the hosting tunneler of the local routes to establish when hosting a service.
* Ziti Controller will now report service posture query policy types (Dial/Bind)
* Ziti Controller now supports enrollment extension for routers
* Ziti Router now support forcing enrollment extension via `run -e`
* Ziti Routers will now automatically extend their enrollment before their certificates expire
* `ziti edge enroll` with a UPDB JWT now confirms and properly sets the password supplied
Caveats:
* Any existing host.v1 configurations that use will become invalid.
* ziti-tunnel and the converged router/tunneler creates local routes that are establised for `allowedSourceAddresses`, but the routes are not consistently cleaned up when `ziti-tunnel` exits. This issue will be addressed in a future release.
# Release 0.19.10
## What's New
* Fixed issue where edge router renames didn't propagate to fabric
* Fixed issue where gateway couldn't dial after router rename
* Allowed parsing identities where values were not URIs
* Allow updating which configs are used by services
# Release 0.19.9
## What's New
+4 -4
View File
@@ -26,8 +26,8 @@ Ziti represents the next generation of secure, open-source networking for your a
**Developer Focus**
* [Open source code, available with the Apache 2.0 license](https://github.com/openziti)
* Fully programable REST management APIs
* [SDKs for a variety of programming languages](~/ziti/clients/sdks.md)
* [Application specific configuration store allowing centralized management of configuration allowing you to add structured configuration specific to your application](~/config-store/overview.md)
* [SDKs for a variety of programming languages](https://openziti.github.io/ziti/clients/sdks)
* [Application specific configuration store allowing centralized management of configuration allowing you to add structured configuration specific to your application](https://openziti.github.io/ziti/config-store/overview)
* An extensible fabric, allowing you to add your own
* load balancing algorithms
* interconnect protocols
@@ -36,9 +36,9 @@ Ziti represents the next generation of secure, open-source networking for your a
* control and management plane messaging and semantics
**Easy Management**
* [A flexible and expressive policy model for managing access to services and edge routers](~/ziti/policies/overview.md)
* [A flexible and expressive policy model for managing access to services and edge routers](https://openziti.github.io/ziti/policies/overview)
* A web based admin console
* [Pre-built tunnelers and proxies for a variety of operating systems, including mobile](~/ziti/clients/tunneler.md)
* [Pre-built tunnelers and proxies for a variety of operating systems, including mobile](https://openziti.github.io/ziti/clients/tunneler)
Let's break some of these buzzwords down.
+18 -2
View File
@@ -148,10 +148,26 @@ func processEnrollment() error {
if tkn.EnrollmentMethod == "updb" {
if password == "" {
password, err = term.PromptPassword("updb enrollment requires a password", false)
password, err = term.PromptPassword("updb enrollment requires a password, please enter one: ", false)
password = strings.TrimSpace(password)
if err != nil {
return fmt.Errorf("failed to complete enrollment, updb requires a non-empty password")
return fmt.Errorf("failed to complete enrollment, updb requires a non-empty password: %v", err)
}
confirm, err := term.PromptPassword("please confirm what you entered: ", false)
if err != nil {
return fmt.Errorf("failed to complete enrollment, updb password confirmation failed: %v", err)
}
confirm = strings.TrimSpace(confirm)
if password != confirm {
return fmt.Errorf("failed to complete enrollment, passwords did not match")
}
flags.Password = password
}
return enroll.EnrollUpdb(flags)
+4 -4
View File
@@ -27,10 +27,10 @@ require (
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d
github.com/keybase/go-ps v0.0.0-20190827175125-91aafc93ba19
github.com/michaelquigley/pfxlog v0.3.7
github.com/openziti/edge v0.19.68
github.com/openziti/fabric v0.16.48
github.com/openziti/foundation v0.15.45
github.com/openziti/sdk-golang v0.15.36
github.com/openziti/edge v0.19.83
github.com/openziti/fabric v0.16.54
github.com/openziti/foundation v0.15.48
github.com/openziti/sdk-golang v0.15.40
github.com/pborman/uuid v1.2.0
github.com/pkg/errors v0.9.1
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0
+9 -11
View File
@@ -335,7 +335,6 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
@@ -377,6 +376,7 @@ github.com/gophercloud/utils v0.0.0-20200508015959-b0167b94122c/go.mod h1:ehWUbL
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/handlers v1.5.0/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
@@ -614,16 +614,14 @@ github.com/openziti/bbolt v1.3.6-0.20210317142109-547da822475e h1:ST9+54UtCr96S5
github.com/openziti/bbolt v1.3.6-0.20210317142109-547da822475e/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
github.com/openziti/dilithium v0.3.3 h1:PLgQ6PMNLSTzCFbX/h98cmudgz/cU6TmjdSv5NAPD8k=
github.com/openziti/dilithium v0.3.3/go.mod h1:vsCjI2AU/hon9e+dLhUFbCNGesJDj2ASgkySOcpmvjo=
github.com/openziti/edge v0.19.68 h1:LDF4UmElo08j1M+y3QPb9Icme35kE5KFao5BzSVQMGA=
github.com/openziti/edge v0.19.68/go.mod h1:Ezv55jngUC7tADADytBPSgoMlj2g2Rd+NQIb0om8Jm4=
github.com/openziti/fabric v0.16.47/go.mod h1:soU2ImwCdNOnLDM5JUqpk5wAvrN9LaslZL02oThJQUM=
github.com/openziti/fabric v0.16.48 h1:tSI/5ekpCukN3qltSTM48yEHT0QopJWUrSbF13JwTuk=
github.com/openziti/fabric v0.16.48/go.mod h1:HLM9JhrqCwB1uM7DLIwA98XU8cCsQDHjs2zs4tf1ASk=
github.com/openziti/foundation v0.15.44/go.mod h1:kYCUttsGO/GjoD7BSNhVOeyYDXHYMAfz59YeLoEyhHY=
github.com/openziti/foundation v0.15.45 h1:fJDXAq/Bd2/n58MggJeFhE4IIwe0YsF+kIZcAkv664c=
github.com/openziti/foundation v0.15.45/go.mod h1:NnSBTp9ZkcYsv6nYxZR0/NzoHfkIo6tjpBHDvBsPFqw=
github.com/openziti/sdk-golang v0.15.36 h1:FBH+lAl8JFze0ZqJqKRyVQ/YAF0sXTX7f6WolcRQLCk=
github.com/openziti/sdk-golang v0.15.36/go.mod h1:i+h3rd5INCvJPBBGXTGZba5eJLznV4AHrZAa88cf6II=
github.com/openziti/edge v0.19.83 h1:+DgptVlQIN9j95uX2LmUBgJmCS7qnBEKVCTeirNT/Vw=
github.com/openziti/edge v0.19.83/go.mod h1:atmbRzo8tAXatznFhQaM3BZlFJZxTq12OIGZazKz7J8=
github.com/openziti/fabric v0.16.54 h1:jfuAv5p73Uu7E2L26TBFoO+ZF5mzeAHgA9CSU8d+rfE=
github.com/openziti/fabric v0.16.54/go.mod h1:fb5zFkB3YQxusAmsQLKKzEk7MmPodqDkqmOVTB+J3eE=
github.com/openziti/foundation v0.15.48 h1:NnL/xY684XeFiLBi5z0+uKf2tpPhlClbMeVmUQPF8Jo=
github.com/openziti/foundation v0.15.48/go.mod h1:NnSBTp9ZkcYsv6nYxZR0/NzoHfkIo6tjpBHDvBsPFqw=
github.com/openziti/sdk-golang v0.15.40 h1:AXAE4qeWf5TLO1ZCamaTTOdfCmWITWyeM4g+RVZDO5U=
github.com/openziti/sdk-golang v0.15.40/go.mod h1:yo85e6HhAL1+PrRjgrsL/ZgkyAcdqVxy4Ws1UMrogss=
github.com/oracle/oci-go-sdk v24.2.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231 h1:fa50YL1pzKW+1SsBnJDOHppJN9stOEwS+CRWyUtyYGU=
-32
View File
@@ -1,32 +0,0 @@
/*
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 (
"github.com/openziti/ziti/ziti-enroller/subcmd"
"github.com/sirupsen/logrus"
)
func main() {
logrus.SetFormatter(&logrus.TextFormatter{
ForceColors: true,
DisableTimestamp: true,
TimestampFormat: "",
PadLevelText: true,
})
subcmd.Execute()
}
-164
View File
@@ -1,164 +0,0 @@
/*
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 subcmd
import (
"encoding/json"
"fmt"
"github.com/michaelquigley/pfxlog"
"github.com/openziti/foundation/identity/certtools"
"github.com/openziti/sdk-golang/ziti/enroll"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"io/ioutil"
"os"
"strings"
)
// global state used by all subcommands are located here for easy discovery
var verbose bool
var jwtpath, outpath, keyPath, certPath, idname, caOverride string
const verboseDesc = "Enable verbose logging."
const outpathDesc = "Output configuration file."
const jwtpathDesc = "Enrollment token (JWT file). Required"
const certDesc = "The certificate to present when establishing a connection."
const idnameDesc = "Names the identity. Ignored if not 3rd party auto enrollment"
const outFlag = "out"
func init() {
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, verboseDesc)
rootCmd.Flags().StringVarP(&jwtpath, "jwt", "j", "", jwtpathDesc)
rootCmd.Flags().StringVarP(&outpath, outFlag, "o", "", outpathDesc)
rootCmd.Flags().StringVarP(&idname, "idname", "n", "", idnameDesc)
rootCmd.Flags().StringVarP(&certPath, "cert", "c", "", certDesc)
rootCmd.Flags().StringVarP(&caOverride, "ca", "", "", "Additional trusted certificates")
var keyDesc = ""
engines := certtools.ListEngines()
if len(engines) > 0 {
keyDesc = fmt.Sprintf("The key to use with the certificate. Optionally specify the engine to use. supported engines: %v", engines)
} else {
keyDesc = fmt.Sprintf("The key to use with the certificate.")
}
rootCmd.Flags().StringVarP(&keyPath, "key", "k", "", keyDesc)
_ = rootCmd.MarkFlagRequired("jwt")
}
var rootCmd = &cobra.Command{
Use: "ziti-enroller",
Short: "Ziti Enroller",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
logrus.Warnf("ziti-enroller is DEPRECATED and will soon be removed. Please use 'ziti edge enroll' or 'ziti-tunnel enroll' instead.")
if verbose {
logrus.SetLevel(logrus.DebugLevel)
}
},
RunE: func(cmd *cobra.Command, args []string) error {
return processEnrollment()
},
}
func Execute() {
rootCmd.SilenceUsage = true
rootCmd.SilenceErrors = true
if err := rootCmd.Execute(); err != nil {
pfxlog.Logger().Errorf("%s\n", err)
}
}
func processEnrollment() error {
if strings.TrimSpace(outpath) == "" {
out, outErr := outPathFromJwt(jwtpath)
if outErr != nil {
return fmt.Errorf("could not set the output path: %s", outErr)
}
outpath = out
}
if jwtpath != "" {
if _, err := os.Stat(jwtpath); os.IsNotExist(err) {
return fmt.Errorf("the provided jwt file does not exist: %s", jwtpath)
}
}
if caOverride != "" {
if _, err := os.Stat(caOverride); os.IsNotExist(err) {
return fmt.Errorf("the provided ca file does not exist: %s", caOverride)
}
}
if strings.TrimSpace(outpath) == strings.TrimSpace(jwtpath) {
return fmt.Errorf("the output path must not be the same as the jwt path")
}
tokenStr, _ := ioutil.ReadFile(jwtpath)
pfxlog.Logger().Debugf("jwt to parse: %s", tokenStr)
tkn, _, err := enroll.ParseToken(string(tokenStr))
if err != nil {
return fmt.Errorf("failed to parse JWT: %s", err.Error())
}
flags := enroll.EnrollmentFlags{
CertFile: certPath,
KeyFile: keyPath,
Token: tkn,
IDName: idname,
AdditionalCAs: caOverride,
KeyAlg: "RSA",
}
conf, err := enroll.Enroll(flags)
if err != nil {
return fmt.Errorf("failed to enroll: %v", err)
}
output, err := os.Create(outpath)
defer output.Close()
if err != nil {
return fmt.Errorf("failed to open file '%s': %s", outpath, err.Error())
}
enc := json.NewEncoder(output)
enc.SetEscapeHTML(false)
encErr := enc.Encode(&conf)
if encErr == nil {
pfxlog.Logger().Infof("enrolled successfully. identity file written to: %s", outpath)
return nil
} else {
return fmt.Errorf("enrollment successful but the identity file was not able to be written to: %s [%s]", outpath, encErr)
}
}
func outPathFromJwt(jwt string) (string, error) {
if strings.HasSuffix(jwt, ".jwt") {
return jwt[:len(jwt)-len(".jwt")] + ".json", nil
} else if strings.HasSuffix(jwt, ".json") {
//ugh - so that makes things a bit uglier but ok fine. we'll return an error in this situation
return "", errors.Errorf("unexpected configuration. cannot infer '%s' flag if the jwt file "+
"ends in .json. rename jwt file or provide the '%s' flag", outFlag, outFlag)
} else {
//doesn't end with .jwt - so just slap a .json on the end and call it a day
return jwt + ".json", nil
}
}
-35
View File
@@ -1,35 +0,0 @@
/*
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 subcmd
import (
"fmt"
"github.com/openziti/ziti/common/version"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(versionCmd)
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Show component version",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(version.GetBuildMetadata(verbose))
},
}
+4
View File
@@ -36,7 +36,11 @@ import (
"syscall"
)
var forceEnrollmentExtension *bool
func init() {
//flags are added to an internal map and read later on, see getFlags()
runCmd.Flags().BoolP("extend", "e", false, "force the router on startup to extend enrollment certificates")
root.AddCommand(runCmd)
}
+3
View File
@@ -1,3 +1,6 @@
# enrollments in-use with Docker Compose
*.json
*.jwt
# executable from fetch bins script
/ziti-tunnel
+4 -4
View File
@@ -53,7 +53,7 @@ the image to a public registry.
Run `docker buildx` like this:
$ ziti_version="0.15.3"
$ ziti_version="0.19.11"
$ docker buildx build \
--platform linux/amd64,linux/arm/v7,linux/aarch64 \
--build-arg ZITI_VERSION="${ziti_version}" \
@@ -88,8 +88,8 @@ This build method produces an image for the CPU that is running the build host
cache.
$ git fetch --tags && git tag -l | sort -Vr | head -1
v0.19.10
$ ziti_version="0.19.10" \
v0.19.11
$ ziti_version="0.19.11" \
$ docker build \
--build-arg ZITI_VERSION="${ziti_version}" \
-t "netfoundry/ziti-tunnel:${ziti_version}" .
@@ -109,4 +109,4 @@ cache.
-c don't check out v${ZITI_VERSION} (use Git working copy)
EXAMPLES
ZITI_VERSION=0.16.1 ./buildx.sh -r netfoundry/ziti-tunnel
ZITI_VERSION=0.19.11 ./buildx.sh -r netfoundry/ziti-tunnel
+13 -6
View File
@@ -1,16 +1,23 @@
#!/bin/bash -eu
set -o pipefail
#!/bin/bash
echo "Fetching from Artifactory."
if [ -z "${ARTIFACTORY_BASE_URL}" ]; then ARTIFACTORY_BASE_URL="https://netfoundry.jfrog.io/netfoundry"; fi
if [ -z "${ARTIFACTORY_REPO}" ]; then ARTIFACTORY_REPO="ziti-release"; fi
set -euo pipefail
[[ $# -eq 0 ]] && {
echo "ERROR: need the base name(s) of the executable(s) to fetch e.g. \"ziti-tunnel ...\"." >&2
exit 1
}
: ${ARTIFACTORY_BASE_URL:="https://netfoundry.jfrog.io/netfoundry"}
: ${ARTIFACTORY_REPO:="ziti-release"}
for var in ARTIFACTORY_BASE_URL ARTIFACTORY_REPO ZITI_VERSION; do
if [ -z "${!var}" ]; then
if [ -z "${!var:-}" ]; then
echo "ERROR: ${var} must be set when fetching binaries from Artifactory." >&2
exit 1
fi
done
echo "Fetching from Artifactory."
# map host architecture/os to directories that we use in netfoundry.jfrog.io.
# (our artifact directories seem to align with Docker's TARGETARCH and TARGETOS
# build arguments, which we could rely on if we fully committed to "docker buildx" - see
@@ -36,6 +36,7 @@ type updateEdgeRouterOptions struct {
roleAttributes []string
tags map[string]string
appData map[string]string
usePut bool
}
func newUpdateEdgeRouterCmd(f cmdutil.Factory, out io.Writer, errOut io.Writer) *cobra.Command {
@@ -67,6 +68,7 @@ func newUpdateEdgeRouterCmd(f cmdutil.Factory, out io.Writer, errOut io.Writer)
"Set role attributes of the edge router. Use --role-attributes '' to set an empty list")
cmd.Flags().StringToStringVar(&options.tags, "tags", nil, "Custom management tags")
cmd.Flags().StringToStringVar(&options.appData, "app-data", nil, "Custom application data")
cmd.Flags().BoolVar(&options.usePut, "use-put", false, "Use PUT to when making the request")
options.AddCommonFlags(cmd)
@@ -111,6 +113,10 @@ func runUpdateEdgeRouter(o *updateEdgeRouterOptions) error {
return errors.New("no change specified. must specify at least one attribute to change")
}
_, err = patchEntityOfType(fmt.Sprintf("edge-routers/%v", id), entityData.String(), &o.edgeOptions)
if o.usePut {
_, err = putEntityOfType(fmt.Sprintf("edge-routers/%v", id), entityData.String(), &o.edgeOptions)
} else {
_, err = patchEntityOfType(fmt.Sprintf("edge-routers/%v", id), entityData.String(), &o.edgeOptions)
}
return err
}
@@ -35,6 +35,7 @@ type updateServiceOptions struct {
terminatorStrategy string
roleAttributes []string
encryption encryptionVar
configs []string
}
func newUpdateServiceCmd(f cmdutil.Factory, out io.Writer, errOut io.Writer) *cobra.Command {
@@ -66,6 +67,7 @@ func newUpdateServiceCmd(f cmdutil.Factory, out io.Writer, errOut io.Writer) *co
"Set role attributes of the service. Use --role-attributes '' to set an empty list")
options.encryption.Set("ON")
cmd.Flags().VarP(&options.encryption, "encryption", "e", "Controls end-to-end encryption for the service")
cmd.Flags().StringSliceVarP(&options.configs, "configs", "c", nil, "Configuration id or names to be associated with the new service")
options.AddCommonFlags(cmd)
return cmd
@@ -100,6 +102,15 @@ func runUpdateService(o *updateServiceOptions) error {
change = true
}
if o.Cmd.Flags().Changed("configs") {
configs, err := mapNamesToIDs("configs", o.edgeOptions, o.configs...)
if err != nil {
return err
}
setJSONValue(entityData, configs, "configs")
change = true
}
if !change {
return errors.New("no change specified. must specify at least one attribute to change")
}