mirror of
https://github.com/redhat-developer/odo.git
synced 2025-10-19 03:06:19 +03:00
param based odo service create for operator backed services (#4704)
* initial work towards param based odo service create * change * add type data to cr * add to devfile * update olm * remove logs * towards tests * init tests * more test * fix tests * fix unit again * set default service name * mod fix * addressed some comments * add postgres operator in setup and in test * fix * fix some tests * fix helpers * helper flag * merge conflicts * add tests and install operator * certain cases fix * fix intg tests * update scripts * fix intg test * changed function name * changes * fix comments * docs update * link fix * small fix * comment * unit tests * doc fix * renamed as per comments * comment
This commit is contained in:
@@ -128,6 +128,48 @@ To run ONE individual test, you can either:
|
||||
|
||||
Integration tests validate and focus on specific fields of odo functionality or individual commands. For example, `cmd_app_test.go` or `generic_test.go`.
|
||||
|
||||
If you are running `operatorhub` tests then you need to install certain operators on the cluster -
|
||||
|
||||
- Etcd Cluster-wide operator
|
||||
- Service Binding operator
|
||||
- Postgres operator
|
||||
|
||||
Etcd and Service Binding operator can be installed by running link:https://github.com/openshift/odo/blob/main/scripts/configure-cluster/common/setup-operators.sh[setup-operator.sh]. To install Postgres operator you can run the following commands
|
||||
|
||||
----
|
||||
oc new-project odo-operator-test
|
||||
# Let developer user have access to the project
|
||||
oc adm policy add-role-to-user edit developer
|
||||
|
||||
oc create -f - <<EOF
|
||||
apiVersion: operators.coreos.com/v1
|
||||
kind: OperatorGroup
|
||||
metadata:
|
||||
generateName: odo-operator-test-
|
||||
namespace: odo-operator-test
|
||||
spec:
|
||||
targetNamespaces:
|
||||
- odo-operator-test
|
||||
EOF
|
||||
|
||||
oc create -f - <<EOF
|
||||
apiVersion: operators.coreos.com/v1alpha1
|
||||
kind: Subscription
|
||||
metadata:
|
||||
name: postgresql-operator-dev4devs-com
|
||||
namespace: odo-operator-test
|
||||
spec:
|
||||
channel: alpha
|
||||
name: postgresql-operator-dev4devs-com
|
||||
source: community-operators
|
||||
sourceNamespace: openshift-marketplace
|
||||
installPlanApproval: "Automatic"
|
||||
EOF
|
||||
|
||||
----
|
||||
|
||||
Note - the `odo-operator-test` is the namespace where postgres operatorhub tests execute by default. You can configure that by setting `REDHAT_POSTGRES_OPERATOR_PROJECT` env variable.
|
||||
|
||||
*E2e tests:*
|
||||
|
||||
E2e (End to end) uses the same library as integration test. E2e tests and test suite files are located in `tests/e2escenarios` directory and can be called using `.PHONY` within `makefile`. Basically end to end (e2e) test contains user specific scenario that is combination of some features/commands in a single test file.
|
||||
|
||||
3
go.mod
3
go.mod
@@ -46,7 +46,8 @@ require (
|
||||
github.com/spf13/cobra v1.1.3
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/tidwall/gjson v1.7.3
|
||||
github.com/tidwall/gjson v1.7.5
|
||||
github.com/tidwall/sjson v1.1.6
|
||||
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
|
||||
github.com/zalando/go-keyring v0.1.1
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect
|
||||
|
||||
8
go.sum
8
go.sum
@@ -999,14 +999,18 @@ github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG
|
||||
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/tchap/go-patricia v2.3.0+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
|
||||
github.com/tebeka/strftime v0.1.3/go.mod h1:7wJm3dZlpr4l/oVK0t1HYIc4rMzQ2XJlOMIUJUJH6XQ=
|
||||
github.com/tidwall/gjson v1.7.3 h1:9dOulDrkCJf1mwljVMhXNQr9ZL2NvajRX7A1R8c6Qxw=
|
||||
github.com/tidwall/gjson v1.7.3/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=
|
||||
github.com/tidwall/gjson v1.7.4 h1:19cchw8FOxkG5mdLRkGf9jqIqEyqdZhPqW60XfyFxk8=
|
||||
github.com/tidwall/gjson v1.7.4/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=
|
||||
github.com/tidwall/gjson v1.7.5 h1:zmAN/xmX7OtpAkv4Ovfso60r/BiCi5IErCDYGNJu+uc=
|
||||
github.com/tidwall/gjson v1.7.5/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=
|
||||
github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
|
||||
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tidwall/pretty v1.1.0 h1:K3hMW5epkdAVwibsQEfR/7Zj0Qgt4DxtNumTq/VloO8=
|
||||
github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tidwall/sjson v1.1.6 h1:8fDdlahON04OZBlTQCIatW8FstSFJz8oxidj5h0rmSQ=
|
||||
github.com/tidwall/sjson v1.1.6/go.mod h1:KN3FZ7odvXIHPbJdhNorK/M9lWweVUbXsXXhrJ/kGOA=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
|
||||
@@ -59,6 +59,22 @@ func (c *Client) GetCustomResourcesFromCSV(csv *olm.ClusterServiceVersion) *[]ol
|
||||
return &csv.Spec.CustomResourceDefinitions.Owned
|
||||
}
|
||||
|
||||
// CheckCustomResourceInCSV checks if the custom resource is present in the CSV.
|
||||
func (c *Client) CheckCustomResourceInCSV(customResource string, csv *olm.ClusterServiceVersion) (bool, *olm.CRDDescription) {
|
||||
var cr *olm.CRDDescription
|
||||
hasCR := false
|
||||
CRs := c.GetCustomResourcesFromCSV(csv)
|
||||
for _, custRes := range *CRs {
|
||||
c := custRes
|
||||
if c.Kind == customResource {
|
||||
cr = &c
|
||||
hasCR = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return hasCR, cr
|
||||
}
|
||||
|
||||
// SearchClusterServiceVersionList searches for whether the operator/CSV contains
|
||||
// given keyword then return it
|
||||
func (c *Client) SearchClusterServiceVersionList(name string) (*olm.ClusterServiceVersionList, error) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
@@ -97,6 +98,17 @@ func (o *CreateOptions) Complete(name string, cmd *cobra.Command, args []string)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// we convert the param list provided in the format of key=value list
|
||||
// to a map
|
||||
o.ParametersMap = make(map[string]string)
|
||||
for _, kv := range o.parameters {
|
||||
kvSlice := strings.Split(kv, "=")
|
||||
// key value not provided in format of key=value
|
||||
if len(kvSlice) != 2 {
|
||||
return errors.New("parameters not provided in key=value format")
|
||||
}
|
||||
o.ParametersMap[kvSlice[0]] = kvSlice[1]
|
||||
}
|
||||
|
||||
err = validDevfileDirectory(o.componentContext)
|
||||
if err != nil {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/openshift/odo/pkg/log"
|
||||
"github.com/openshift/odo/pkg/service"
|
||||
svc "github.com/openshift/odo/pkg/service"
|
||||
olm "github.com/operator-framework/api/pkg/operators/v1alpha1"
|
||||
"github.com/pkg/errors"
|
||||
@@ -121,17 +122,30 @@ func (b *OperatorBackend) ValidateServiceCreate(o *CreateOptions) (err error) {
|
||||
// k8s does't have it installed by default but OCP does
|
||||
return err
|
||||
}
|
||||
|
||||
almExample, err := svc.GetAlmExample(csv, b.CustomResource, o.ServiceType)
|
||||
b.group, b.version, b.resource, err = svc.GetGVRFromOperator(csv, b.CustomResource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.OriginalCRD = almExample
|
||||
// if the service name is blank then we set it to custom resource name
|
||||
if o.ServiceName == "" {
|
||||
o.ServiceName = strings.ToLower(b.CustomResource)
|
||||
}
|
||||
|
||||
b.group, b.version, b.resource, err = svc.GetGVRFromOperator(csv, b.CustomResource)
|
||||
if err != nil {
|
||||
return err
|
||||
if len(o.parameters) != 0 {
|
||||
builtCRD, err := b.buildCRDfromParams(o, csv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.OriginalCRD = builtCRD
|
||||
} else {
|
||||
almExample, err := svc.GetAlmExample(csv, b.CustomResource, o.ServiceType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.OriginalCRD = almExample
|
||||
}
|
||||
|
||||
if o.ServiceName != "" && !o.DryRun {
|
||||
@@ -147,7 +161,6 @@ func (b *OperatorBackend) ValidateServiceCreate(o *CreateOptions) (err error) {
|
||||
|
||||
d.SetServiceName(o.ServiceName)
|
||||
}
|
||||
|
||||
err = d.ValidateMetadataInCRD()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -240,3 +253,12 @@ func (b *OperatorBackend) DeleteService(o *DeleteOptions, name string, applicati
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *OperatorBackend) buildCRDfromParams(o *CreateOptions, csv olm.ClusterServiceVersion) (map[string]interface{}, error) {
|
||||
hasCR, cr := o.KClient.CheckCustomResourceInCSV(b.CustomResource, &csv)
|
||||
if !hasCR {
|
||||
return nil, fmt.Errorf("the %q resource doesn't exist in specified %q operator", b.CustomResource, o.ServiceType)
|
||||
}
|
||||
|
||||
return service.BuildCRDFromParams(cr, o.ParametersMap)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -71,17 +70,6 @@ func (b *ServiceCatalogBackend) CompleteServiceCreate(o *CreateOptions, cmd *cob
|
||||
o.ServiceName = o.ServiceType
|
||||
}
|
||||
|
||||
// we convert the param list provided in the format of key=value list
|
||||
// to a map
|
||||
o.ParametersMap = make(map[string]string)
|
||||
for _, kv := range o.parameters {
|
||||
kvSlice := strings.Split(kv, "=")
|
||||
// key value not provided in format of key=value
|
||||
if len(kvSlice) != 2 {
|
||||
return errors.New("parameters not provided in key=value format")
|
||||
}
|
||||
o.ParametersMap[kvSlice[0]] = kvSlice[1]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
68
pkg/service/crd_builder.go
Normal file
68
pkg/service/crd_builder.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
olm "github.com/operator-framework/api/pkg/operators/v1alpha1"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// CRDBuilder is responsible for build the full CR including the meta and spec.
|
||||
type CRDBuilder struct {
|
||||
CRDSpecBuilder *CRDSpecBuilder
|
||||
crd *olm.CRDDescription
|
||||
cr map[string]interface{}
|
||||
}
|
||||
|
||||
func NewCRDBuilder(crd *olm.CRDDescription) *CRDBuilder {
|
||||
return &CRDBuilder{
|
||||
CRDSpecBuilder: NewCRDSpecBuilder(crd.SpecDescriptors),
|
||||
crd: crd,
|
||||
cr: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (crb *CRDBuilder) SetAndValidate(param string, value string) error {
|
||||
return crb.CRDSpecBuilder.SetAndValidate(param, value)
|
||||
}
|
||||
|
||||
func (crb *CRDBuilder) Map() (map[string]interface{}, error) {
|
||||
group, version, _, err := GetGVRFromCR(crb.crd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
crb.cr["apiVersion"] = group + "/" + version
|
||||
crb.cr["kind"] = crb.crd.Kind
|
||||
crb.cr["metadata"] = make(map[string]interface{})
|
||||
specMap, err := crb.CRDSpecBuilder.Map()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
crb.cr["spec"] = specMap
|
||||
return crb.cr, nil
|
||||
}
|
||||
|
||||
// BuildCRDFromParams iterates over the parameter maps provided by the user and builds the CRD
|
||||
func BuildCRDFromParams(cr *olm.CRDDescription, paramMap map[string]string) (map[string]interface{}, error) {
|
||||
|
||||
crBuilder := NewCRDBuilder(cr)
|
||||
var errorStrs []string
|
||||
|
||||
for key, value := range paramMap {
|
||||
err := crBuilder.SetAndValidate(key, value)
|
||||
if err != nil {
|
||||
errorStrs = append(errorStrs, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if len(errorStrs) > 0 {
|
||||
return nil, errors.New(strings.Join(errorStrs, "\n"))
|
||||
}
|
||||
|
||||
builtCRD, err := crBuilder.Map()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return builtCRD, nil
|
||||
}
|
||||
97
pkg/service/crd_builder_test.go
Normal file
97
pkg/service/crd_builder_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
olm "github.com/operator-framework/api/pkg/operators/v1alpha1"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// MockCRDescriptionOne a mock description
|
||||
func MockCRDDescriptionOne() *olm.CRDDescription {
|
||||
return &olm.CRDDescription{
|
||||
Name: "etcdclusters.etcd.database.coreos.com",
|
||||
Version: "v1beta2",
|
||||
Kind: "EtcdCluster",
|
||||
DisplayName: "etcd Cluster",
|
||||
Resources: []olm.APIResourceReference{
|
||||
{Kind: "Service", Version: "v1"},
|
||||
{Kind: "Pod", Version: "v1"},
|
||||
},
|
||||
SpecDescriptors: []olm.SpecDescriptor{
|
||||
{
|
||||
Path: "size",
|
||||
DisplayName: "Size",
|
||||
Description: "The desired number of member Pods for the etcd cluster.",
|
||||
XDescriptors: []string{
|
||||
"urn:alm:descriptor:com.tectonic.ui:podCount",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "pod.resources",
|
||||
DisplayName: "Resource Requirements",
|
||||
Description: "Limits describes the minimum/maximum amount of compute resources required/allowed",
|
||||
XDescriptors: []string{
|
||||
"urn:alm:descriptor:com.tectonic.ui:resourceRequirements",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MockCRDescriptionTwo a mock description
|
||||
func MockCRDDescriptionTwo() *olm.CRDDescription {
|
||||
return &olm.CRDDescription{
|
||||
Name: "pgclusters.crunchydata.com",
|
||||
Version: "v1",
|
||||
Kind: "Pgcluster",
|
||||
DisplayName: "Postgres Primary Cluster Member",
|
||||
Description: "Represents a Postgres primary cluster member",
|
||||
Resources: []olm.APIResourceReference{
|
||||
{Kind: "Pgcluster", Version: "v1"},
|
||||
{Kind: "ConfigMap", Version: "v1"},
|
||||
{Kind: "Deployment", Version: "v1"},
|
||||
{Kind: "Job", Version: "v1"},
|
||||
{Kind: "Pod", Version: "v1"},
|
||||
{Kind: "ReplicaSet", Version: "v1"},
|
||||
{Kind: "Secret", Version: "v1"},
|
||||
{Kind: "Service", Version: "v1"},
|
||||
{Kind: "PersistentVolumeClaim", Version: "v1"},
|
||||
},
|
||||
SpecDescriptors: []olm.SpecDescriptor{
|
||||
{
|
||||
Path: "cppimage",
|
||||
DisplayName: "PostgreSQL Image",
|
||||
Description: "The Crunchy PostgreSQL image to use. Possible values are \"crunchy-postgres-ha\" and \"crunchy-postgres-gis-ha\"",
|
||||
},
|
||||
{
|
||||
Path: "cppimagetag",
|
||||
DisplayName: "PostgresSQL Image Tag",
|
||||
Description: "The tag of the PostgreSQL image to use. Example is \"ubi7-12.4-4.5.0\"",
|
||||
},
|
||||
{
|
||||
Path: "host",
|
||||
DisplayName: "PostgreSQL Host",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
func TestCRDBuilderMap(t *testing.T) {
|
||||
builder := NewCRDBuilder(MockCRDDescriptionTwo())
|
||||
require.Nil(t, builder.SetAndValidate("host", "10.1.10.2"), "set shouldn't fail")
|
||||
require.Nil(t, builder.SetAndValidate("cppimage", "crunchy-postgres-ha"), "set shouldn't fail")
|
||||
require.Nil(t, builder.SetAndValidate("cppimagetag", "2.5"), "set shouldn't fail")
|
||||
outMap, err := builder.Map()
|
||||
require.Nil(t, err, "error shouldn't be nil")
|
||||
expected := map[string]interface{}{
|
||||
"apiVersion": "crunchydata.com/v1",
|
||||
"kind": "Pgcluster",
|
||||
"metadata": map[string]interface{}{},
|
||||
"spec": map[string]interface{}{
|
||||
"cppimage": "crunchy-postgres-ha",
|
||||
"cppimagetag": 2.5,
|
||||
"host": "10.1.10.2",
|
||||
},
|
||||
}
|
||||
require.Equal(t, outMap, expected, "The map output doesn't match the expected out")
|
||||
}
|
||||
90
pkg/service/crd_spec_builder.go
Normal file
90
pkg/service/crd_spec_builder.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
olm "github.com/operator-framework/api/pkg/operators/v1alpha1"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// CRDSpecBuilder provides all the functionalities to validate and build operands (operators) spec
|
||||
// based on schema available for them.
|
||||
type CRDSpecBuilder struct {
|
||||
descriptors []olm.SpecDescriptor
|
||||
|
||||
builtJsonStr string
|
||||
params map[string]interface{}
|
||||
}
|
||||
|
||||
func NewCRDSpecBuilder(descriptors []olm.SpecDescriptor) *CRDSpecBuilder {
|
||||
return &CRDSpecBuilder{
|
||||
params: make(map[string]interface{}),
|
||||
descriptors: descriptors,
|
||||
}
|
||||
}
|
||||
|
||||
// set sets the param. The param is provided in json path format. e.g. "first.name".
|
||||
// It is also responsible for parsing the values from string to an appropriate type.
|
||||
func (pb *CRDSpecBuilder) set(param string, value string) error {
|
||||
parsedValue := pb.convertType(value)
|
||||
pb.params[param] = parsedValue
|
||||
tJsonStr, err := sjson.Set(pb.builtJsonStr, param, parsedValue)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error while setting param value for operand")
|
||||
}
|
||||
pb.builtJsonStr = tJsonStr
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pb *CRDSpecBuilder) convertType(value string) interface{} {
|
||||
intv, err := strconv.ParseInt(value, 10, 64)
|
||||
if err == nil {
|
||||
return int64(intv)
|
||||
}
|
||||
floatv, err := strconv.ParseFloat(value, 64)
|
||||
|
||||
if err == nil {
|
||||
return floatv
|
||||
}
|
||||
|
||||
boolv, err := strconv.ParseBool(value)
|
||||
if err == nil {
|
||||
return boolv
|
||||
}
|
||||
// if there were errors for everything else we return the value
|
||||
return value
|
||||
}
|
||||
|
||||
// SetAndValidate validates if a param is part of the operand schema and then sets it.
|
||||
func (pb *CRDSpecBuilder) SetAndValidate(param string, value string) error {
|
||||
if pb.hasParam(param) {
|
||||
return pb.set(param, value)
|
||||
}
|
||||
return fmt.Errorf("the parameter %s is not present in the Operand Schema", param)
|
||||
}
|
||||
|
||||
func (pb *CRDSpecBuilder) hasParam(param string) bool {
|
||||
for _, desc := range pb.descriptors {
|
||||
if desc.Path == param {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Map returns the final map
|
||||
func (pb *CRDSpecBuilder) Map() (map[string]interface{}, error) {
|
||||
var out map[string]interface{}
|
||||
|
||||
err := json.Unmarshal([]byte(pb.builtJsonStr), &out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// JSON returns the final json string
|
||||
func (pb *CRDSpecBuilder) JSON() string {
|
||||
return pb.builtJsonStr
|
||||
}
|
||||
25
pkg/service/crd_spec_builder_test.go
Normal file
25
pkg/service/crd_spec_builder_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCRDSpecBuilderSetAndValidate(t *testing.T) {
|
||||
builder := NewCRDSpecBuilder(MockCRDDescriptionOne().SpecDescriptors)
|
||||
err := builder.SetAndValidate("size", "3")
|
||||
require.Nil(t, err, "set shouldn't fail")
|
||||
result := builder.JSON()
|
||||
require.Equal(t, result, `{"size":3}`)
|
||||
// second time to confirm it doesn't duplicate
|
||||
err = builder.SetAndValidate("size", "3")
|
||||
require.Nil(t, err, "set shouldn't fail")
|
||||
result = builder.JSON()
|
||||
require.Equal(t, result, `{"size":3}`)
|
||||
// incorrect argument
|
||||
err = builder.SetAndValidate("seze", "3")
|
||||
require.NotNil(t, err, "set should fail")
|
||||
require.Equal(t, err.Error(), fmt.Sprintf("the parameter %s is not present in the Operand Schema", "seze"), "error statement should match")
|
||||
}
|
||||
@@ -820,9 +820,8 @@ func (d *DynamicCRD) SetServiceName(name string) {
|
||||
metaMap[k] = name
|
||||
return
|
||||
}
|
||||
// if metadata doesn't have 'name' field, we set it up
|
||||
metaMap["name"] = name
|
||||
}
|
||||
metaMap["name"] = name
|
||||
}
|
||||
|
||||
// GetServiceNameFromCRD fetches the service name from metadata.name field of the CRD
|
||||
|
||||
@@ -36,6 +36,7 @@ install_service_binding_operator(){
|
||||
EOF
|
||||
}
|
||||
|
||||
|
||||
# install etcd operator
|
||||
|
||||
install_etcd_operator
|
||||
|
||||
28
scripts/configure-cluster/common/setup-postgres-operator.sh
Normal file
28
scripts/configure-cluster/common/setup-postgres-operator.sh
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
|
||||
install_postgres_operator(){
|
||||
oc create -f - <<EOF
|
||||
apiVersion: operators.coreos.com/v1
|
||||
kind: OperatorGroup
|
||||
metadata:
|
||||
generateName: ${1}-
|
||||
namespace: ${1}
|
||||
spec:
|
||||
targetNamespaces:
|
||||
- ${1}
|
||||
EOF
|
||||
|
||||
oc create -f - <<EOF
|
||||
apiVersion: operators.coreos.com/v1alpha1
|
||||
kind: Subscription
|
||||
metadata:
|
||||
name: postgresql-operator-dev4devs-com
|
||||
namespace: ${1}
|
||||
spec:
|
||||
channel: alpha
|
||||
name: postgresql-operator-dev4devs-com
|
||||
source: community-operators
|
||||
sourceNamespace: openshift-marketplace
|
||||
installPlanApproval: "Automatic"
|
||||
EOF
|
||||
}
|
||||
@@ -5,16 +5,20 @@ set -x
|
||||
LIBDIR="./scripts/configure-cluster"
|
||||
LIBCOMMON="$LIBDIR/common"
|
||||
SETUP_OPERATORS="$LIBCOMMON/setup-operators.sh"
|
||||
SETUP_POSTGRES_OPERATOR="$LIBCOMMON/setup-postgres-operator.sh"
|
||||
AUTH_SCRIPT="$LIBCOMMON/auth.sh"
|
||||
KUBEADMIN_SCRIPT="$LIBCOMMON/kubeconfigandadmin.sh"
|
||||
|
||||
CI_OPERATOR_HUB_PROJECT="ci-operator-hub-project"
|
||||
POSTGRES_OPERATOR_PROJECT="odo-operator-test"
|
||||
|
||||
# list of namespace to create
|
||||
IMAGE_TEST_NAMESPACES="openjdk-11-rhel8 nodejs-12-rhel7 nodejs-12 openjdk-11 nodejs-14"
|
||||
|
||||
. $KUBEADMIN_SCRIPT
|
||||
|
||||
. $SETUP_POSTGRES_OPERATOR
|
||||
|
||||
# Setup the cluster for Operator tests
|
||||
|
||||
# Create a new namesapce which will be used for OperatorHub checks
|
||||
@@ -23,6 +27,13 @@ oc new-project $CI_OPERATOR_HUB_PROJECT
|
||||
oc adm policy add-role-to-user edit developer
|
||||
|
||||
sh $SETUP_OPERATORS
|
||||
|
||||
oc new-project $POSTGRES_OPERATOR_PROJECT
|
||||
# Let developer user have access to the project
|
||||
oc adm policy add-role-to-user edit developer
|
||||
|
||||
install_postgres_operator $POSTGRES_OPERATOR_PROJECT
|
||||
|
||||
# OperatorHub setup complete
|
||||
|
||||
# Create the namespace for e2e image test apply pull secret to the namespace
|
||||
|
||||
@@ -6,6 +6,7 @@ shout() {
|
||||
set -x
|
||||
}
|
||||
|
||||
|
||||
set -ex
|
||||
|
||||
shout "Setting up some stuff"
|
||||
@@ -19,6 +20,9 @@ export GOBIN="`pwd`/bin"
|
||||
export KUBECONFIG="`pwd`/config"
|
||||
export ARTIFACTS_DIR="`pwd`/artifacts"
|
||||
export CUSTOM_HOMEDIR=$ARTIFACT_DIR
|
||||
LIBDIR="./scripts/configure-cluster"
|
||||
LIBCOMMON="$LIBDIR/common"
|
||||
SETUP_POSTGRES_OPERATOR="$LIBCOMMON/setup-postgres-operator.sh"
|
||||
|
||||
# This si one of the variables injected by ci-firewall. Its purpose is to allow scripts to handle uniqueness as needed
|
||||
SCRIPT_IDENTITY=${SCRIPT_IDENTITY:-"def-id"}
|
||||
@@ -88,9 +92,10 @@ export REDHAT_OPENJDK11_UBI8_PROJECT="${SCRIPT_IDENTITY}$(cat /dev/urandom | tr
|
||||
export REDHAT_NODEJS12_RHEL7_PROJECT="${SCRIPT_IDENTITY}$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 4 | head -n 1)"
|
||||
export REDHAT_NODEJS12_UBI8_PROJECT="${SCRIPT_IDENTITY}$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 4 | head -n 1)"
|
||||
export REDHAT_NODEJS14_UBI8_PROJECT="${SCRIPT_IDENTITY}$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 4 | head -n 1)"
|
||||
export REDHAT_POSTGRES_OPERATOR_PROJECT="${SCRIPT_IDENTITY}$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 4 | head -n 1)"
|
||||
|
||||
# Create the namespace for e2e image test apply pull secret to the namespace
|
||||
for i in `echo "$REDHAT_OPENJDK11_RHEL8_PROJECT $REDHAT_NODEJS12_RHEL7_PROJECT $REDHAT_NODEJS12_UBI8_PROJECT $REDHAT_OPENJDK11_UBI8_PROJECT $REDHAT_NODEJS14_UBI8_PROJECT"`; do
|
||||
for i in `echo "$REDHAT_OPENJDK11_RHEL8_PROJECT $REDHAT_NODEJS12_RHEL7_PROJECT $REDHAT_NODEJS12_UBI8_PROJECT $REDHAT_OPENJDK11_UBI8_PROJECT $REDHAT_NODEJS14_UBI8_PROJECT $REDHAT_POSTGRES_OPERATOR_PROJECT"`; do
|
||||
# create the namespace
|
||||
oc new-project $i
|
||||
# Applying pull secret to the namespace which will be used for pulling images from authenticated registry
|
||||
@@ -99,6 +104,17 @@ for i in `echo "$REDHAT_OPENJDK11_RHEL8_PROJECT $REDHAT_NODEJS12_RHEL7_PROJECT $
|
||||
oc adm policy add-role-to-user edit developer
|
||||
done
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# Install namespace specific operators
|
||||
|
||||
. $SETUP_POSTGRES_OPERATOR
|
||||
|
||||
install_postgres_operator $REDHAT_POSTGRES_OPERATOR_PROJECT
|
||||
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
shout "Logging into 4x cluster as developer (logs hidden)"
|
||||
set +x
|
||||
oc login -u developer -p ${OCP4X_DEVELOPER_PASSWORD} --insecure-skip-tls-verify ${OCP4X_API_URL}
|
||||
|
||||
@@ -15,6 +15,7 @@ type CliRunner interface {
|
||||
GetServices(namespace string) string
|
||||
CreateRandNamespaceProject() string
|
||||
CreateRandNamespaceProjectOfLength(i int) string
|
||||
SetProject(namespace string) string
|
||||
DeleteNamespaceProject(projectName string)
|
||||
DeletePod(podName string, projectName string)
|
||||
GetEnvsDevFileDeployment(componentName, appName, projectName string) map[string]string
|
||||
|
||||
@@ -165,6 +165,13 @@ func (kubectl KubectlRunner) createRandNamespaceProject(projectName string) stri
|
||||
return projectName
|
||||
}
|
||||
|
||||
func (kubectl KubectlRunner) SetProject(namespace string) string {
|
||||
Cmd("kubectl", "config", "set-context", "--current", "--namespace", namespace).ShouldPass()
|
||||
session := Cmd("kubectl", "get", "namespaces").ShouldPass().Out()
|
||||
Expect(session).To(ContainSubstring(namespace))
|
||||
return namespace
|
||||
}
|
||||
|
||||
// CreateRandNamespaceProjectOfLength create new project with i as the length of the name
|
||||
func (kubectl KubectlRunner) CreateRandNamespaceProjectOfLength(i int) string {
|
||||
projectName := RandString(i)
|
||||
|
||||
@@ -631,6 +631,12 @@ func (oc OcRunner) createRandNamespaceProject(projectName string) string {
|
||||
return projectName
|
||||
}
|
||||
|
||||
func (oc OcRunner) SetProject(namespace string) string {
|
||||
fmt.Fprintf(GinkgoWriter, "Setting project: %s\n", namespace)
|
||||
Cmd("odo", "project", "set", namespace).ShouldPass()
|
||||
return namespace
|
||||
}
|
||||
|
||||
// DeleteNamespaceProject deletes a specified project in oc cluster
|
||||
func (oc OcRunner) DeleteNamespaceProject(projectName string) {
|
||||
fmt.Fprintf(GinkgoWriter, "Deleting project: %s\n", projectName)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/openshift/odo/pkg/util"
|
||||
"github.com/openshift/odo/tests/helper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
@@ -51,6 +52,68 @@ var _ = Describe("odo service command tests for OperatorHub", func() {
|
||||
Expect(stdOut).To(ContainSubstring("service can be created/deleted from a valid component directory only"))
|
||||
})
|
||||
|
||||
Context("a namespace specific operator is installed", func() {
|
||||
|
||||
var postgresOperator string
|
||||
var postgresDatabase string
|
||||
var projectName string
|
||||
|
||||
JustBeforeEach(func() {
|
||||
projectName = util.GetEnvWithDefault("REDHAT_POSTGRES_OPERATOR_PROJECT", "odo-operator-test")
|
||||
helper.GetCliRunner().SetProject(projectName)
|
||||
operators := helper.Cmd("odo", "catalog", "list", "services").ShouldPass().Out()
|
||||
postgresOperator = regexp.MustCompile(`postgresql-operator\.*[a-z][0-9]\.[0-9]\.[0-9]`).FindString(operators)
|
||||
postgresDatabase = fmt.Sprintf("%s/Database", postgresOperator)
|
||||
})
|
||||
|
||||
When("a nodejs component is created", func() {
|
||||
|
||||
JustBeforeEach(func() {
|
||||
helper.Cmd("odo", "create", "nodejs").ShouldPass().Out()
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
// we do this because for these specific tests we dont delete the project
|
||||
helper.Cmd("odo", "delete", "--all", "-f").ShouldPass().Out()
|
||||
})
|
||||
|
||||
When("creating a postgres operand with params", func() {
|
||||
var operandName string
|
||||
|
||||
JustBeforeEach(func() {
|
||||
operandName = helper.RandString(10)
|
||||
helper.Cmd("odo", "service", "create", postgresDatabase, operandName, "-p",
|
||||
"databaseName=odo", "-p", "size=1", "-p", "databaseUser=odo", "-p",
|
||||
"databaseStorageRequest=1Gi", "-p", "databasePassword=odopasswd").ShouldPass().Out()
|
||||
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
helper.Cmd("odo", "service", "delete", fmt.Sprintf("Database/%s", operandName), "-f").ShouldPass().Out()
|
||||
helper.Cmd("odo", "push").ShouldPass().Out()
|
||||
})
|
||||
|
||||
When("odo push is executed", func() {
|
||||
JustBeforeEach(func() {
|
||||
helper.Cmd("odo", "push").ShouldPass().Out()
|
||||
})
|
||||
|
||||
It("should create pods in running state", func() {
|
||||
oc.PodsShouldBeRunning(projectName, fmt.Sprintf(`%s-.[\-a-z0-9]*`, operandName))
|
||||
})
|
||||
|
||||
It("should list the service", func() {
|
||||
// now test listing of the service using odo
|
||||
stdOut := helper.Cmd("odo", "service", "list").ShouldPass().Out()
|
||||
Expect(stdOut).To(ContainSubstring(fmt.Sprintf("Database/%s", operandName)))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
Context("a specific operator is installed", func() {
|
||||
var etcdOperator string
|
||||
var etcdCluster string
|
||||
@@ -218,7 +281,7 @@ var _ = Describe("odo service command tests for OperatorHub", func() {
|
||||
devfilePath := filepath.Join(commonVar.Context, "devfile.yaml")
|
||||
content, err := ioutil.ReadFile(devfilePath)
|
||||
Expect(err).To(BeNil())
|
||||
matchInOutput := []string{"kubernetes", "inlined", "EtcdCluster", "example"}
|
||||
matchInOutput := []string{"kubernetes", "inlined", "EtcdCluster", "etcdcluster"}
|
||||
helper.MatchAllInOutput(string(content), matchInOutput)
|
||||
})
|
||||
|
||||
@@ -229,24 +292,24 @@ var _ = Describe("odo service command tests for OperatorHub", func() {
|
||||
})
|
||||
|
||||
It("should create pods in running state", func() {
|
||||
oc.PodsShouldBeRunning(commonVar.Project, `example-.[a-z0-9]*`)
|
||||
oc.PodsShouldBeRunning(commonVar.Project, `etcdcluster-.[a-z0-9]*`)
|
||||
})
|
||||
|
||||
It("should list the service", func() {
|
||||
// now test listing of the service using odo
|
||||
stdOut := helper.Cmd("odo", "service", "list").ShouldPass().Out()
|
||||
Expect(stdOut).To(ContainSubstring("EtcdCluster/example"))
|
||||
Expect(stdOut).To(ContainSubstring("EtcdCluster/etcdcluster"))
|
||||
})
|
||||
|
||||
It("should list the service in JSON format", func() {
|
||||
jsonOut := helper.Cmd("odo", "service", "list", "-o", "json").ShouldPass().Out()
|
||||
helper.MatchAllInOutput(jsonOut, []string{"\"apiVersion\": \"etcd.database.coreos.com/v1beta2\"", "\"kind\": \"EtcdCluster\"", "\"name\": \"example\""})
|
||||
helper.MatchAllInOutput(jsonOut, []string{"\"apiVersion\": \"etcd.database.coreos.com/v1beta2\"", "\"kind\": \"EtcdCluster\"", "\"name\": \"etcdcluster\""})
|
||||
})
|
||||
|
||||
When("a link is created with the service", func() {
|
||||
var stdOut string
|
||||
JustBeforeEach(func() {
|
||||
stdOut = helper.Cmd("odo", "link", "EtcdCluster/example").ShouldPass().Out()
|
||||
stdOut = helper.Cmd("odo", "link", "EtcdCluster/etcdcluster").ShouldPass().Out()
|
||||
})
|
||||
|
||||
It("should display a successful message", func() {
|
||||
@@ -260,13 +323,13 @@ var _ = Describe("odo service command tests for OperatorHub", func() {
|
||||
if os.Getenv("KUBERNETES") == "true" {
|
||||
Skip("This is a OpenShift specific scenario, skipping")
|
||||
}
|
||||
stdOut = helper.Cmd("odo", "link", "EtcdCluster/example").ShouldFail().Err()
|
||||
stdOut = helper.Cmd("odo", "link", "EtcdCluster/etcdcluster").ShouldFail().Err()
|
||||
Expect(stdOut).To(ContainSubstring("already linked with the service"))
|
||||
})
|
||||
|
||||
When("the link is deleted", func() {
|
||||
JustBeforeEach(func() {
|
||||
stdOut = helper.Cmd("odo", "unlink", "EtcdCluster/example").ShouldPass().Out()
|
||||
stdOut = helper.Cmd("odo", "unlink", "EtcdCluster/etcdcluster").ShouldPass().Out()
|
||||
})
|
||||
|
||||
It("should display a successful message", func() {
|
||||
@@ -280,7 +343,7 @@ var _ = Describe("odo service command tests for OperatorHub", func() {
|
||||
if os.Getenv("KUBERNETES") == "true" {
|
||||
Skip("This is a OpenShift specific scenario, skipping")
|
||||
}
|
||||
stdOut = helper.Cmd("odo", "unlink", "EtcdCluster/example").ShouldFail().Err()
|
||||
stdOut = helper.Cmd("odo", "unlink", "EtcdCluster/etcdcluster").ShouldFail().Err()
|
||||
Expect(stdOut).To(ContainSubstring("failed to unlink the service"))
|
||||
})
|
||||
})
|
||||
@@ -288,7 +351,7 @@ var _ = Describe("odo service command tests for OperatorHub", func() {
|
||||
|
||||
When("the service is deleted", func() {
|
||||
JustBeforeEach(func() {
|
||||
helper.Cmd("odo", "service", "delete", "EtcdCluster/example", "-f").ShouldPass()
|
||||
helper.Cmd("odo", "service", "delete", "EtcdCluster/etcdcluster", "-f").ShouldPass()
|
||||
})
|
||||
|
||||
It("should delete service definition from devfile.yaml", func() {
|
||||
@@ -296,12 +359,12 @@ var _ = Describe("odo service command tests for OperatorHub", func() {
|
||||
devfilePath := filepath.Join(commonVar.Context, "devfile.yaml")
|
||||
content, err := ioutil.ReadFile(devfilePath)
|
||||
Expect(err).To(BeNil())
|
||||
matchInOutput := []string{"kubernetes", "inlined", "EtcdCluster", "example"}
|
||||
matchInOutput := []string{"kubernetes", "inlined", "EtcdCluster", "etcdcluster"}
|
||||
helper.DontMatchAllInOutput(string(content), matchInOutput)
|
||||
})
|
||||
|
||||
It("should fail to delete the service again", func() {
|
||||
stdOut = helper.Cmd("odo", "service", "delete", "EtcdCluster/example", "-f").ShouldFail().Err()
|
||||
stdOut = helper.Cmd("odo", "service", "delete", "EtcdCluster/etcdcluster", "-f").ShouldFail().Err()
|
||||
Expect(stdOut).To(ContainSubstring("couldn't find service named"))
|
||||
})
|
||||
|
||||
@@ -339,7 +402,7 @@ var _ = Describe("odo service command tests for OperatorHub", func() {
|
||||
It("should list both services", func() {
|
||||
stdOut = helper.Cmd("odo", "service", "list").ShouldPass().Out()
|
||||
// first service still here
|
||||
Expect(stdOut).To(ContainSubstring("EtcdCluster/example"))
|
||||
Expect(stdOut).To(ContainSubstring("EtcdCluster/etcdcluster"))
|
||||
// second service created
|
||||
Expect(stdOut).To(ContainSubstring("EtcdCluster/myetcd2"))
|
||||
})
|
||||
|
||||
8
vendor/github.com/tidwall/gjson/README.md
generated
vendored
8
vendor/github.com/tidwall/gjson/README.md
generated
vendored
@@ -150,10 +150,6 @@ result.Less(token Result, caseSensitive bool) bool
|
||||
|
||||
The `result.Value()` function returns an `interface{}` which requires type assertion and is one of the following Go types:
|
||||
|
||||
The `result.Array()` function returns back an array of values.
|
||||
If the result represents a non-existent value, then an empty array will be returned.
|
||||
If the result is not a JSON array, the return value will be an array containing one result.
|
||||
|
||||
```go
|
||||
boolean >> bool
|
||||
number >> float64
|
||||
@@ -163,6 +159,10 @@ array >> []interface{}
|
||||
object >> map[string]interface{}
|
||||
```
|
||||
|
||||
The `result.Array()` function returns back an array of values.
|
||||
If the result represents a non-existent value, then an empty array will be returned.
|
||||
If the result is not a JSON array, the return value will be an array containing one result.
|
||||
|
||||
### 64-bit integers
|
||||
|
||||
The `result.Int()` and `result.Uint()` calls are capable of reading all 64 bits, allowing for large JSON integers.
|
||||
|
||||
17
vendor/github.com/tidwall/gjson/SYNTAX.md
generated
vendored
17
vendor/github.com/tidwall/gjson/SYNTAX.md
generated
vendored
@@ -77,14 +77,21 @@ Special purpose characters, such as `.`, `*`, and `?` can be escaped with `\`.
|
||||
fav\.movie "Deer Hunter"
|
||||
```
|
||||
|
||||
You'll also need to make sure that the `\` character is correctly escaped when hardcoding a path in source code.
|
||||
You'll also need to make sure that the `\` character is correctly escaped when hardcoding a path in you source code.
|
||||
|
||||
```go
|
||||
res := gjson.Get(json, "fav\\.movie") // must escape the slash
|
||||
res := gjson.Get(json, `fav\.movie`) // no need to escape the slash
|
||||
|
||||
// Go
|
||||
val := gjson.Get(json, "fav\\.movie") // must escape the slash
|
||||
val := gjson.Get(json, `fav\.movie`) // no need to escape the slash
|
||||
```
|
||||
|
||||
```rust
|
||||
// Rust
|
||||
let val = gjson::get(json, "fav\\.movie") // must escape the slash
|
||||
let val = gjson::get(json, r#"fav\.movie"#) // no need to escape the slash
|
||||
```
|
||||
|
||||
|
||||
### Arrays
|
||||
|
||||
The `#` character allows for digging into JSON Arrays.
|
||||
@@ -248,6 +255,8 @@ gjson.AddModifier("case", func(json, arg string) string {
|
||||
"children.@case:lower.@reverse" ["jack","alex","sara"]
|
||||
```
|
||||
|
||||
*Note: Custom modifiers are not yet available in the Rust version*
|
||||
|
||||
### Multipaths
|
||||
|
||||
Starting with v1.3.0, GJSON added the ability to join multiple paths together
|
||||
|
||||
151
vendor/github.com/tidwall/gjson/gjson.go
generated
vendored
151
vendor/github.com/tidwall/gjson/gjson.go
generated
vendored
@@ -714,10 +714,10 @@ type arrayPathResult struct {
|
||||
alogkey string
|
||||
query struct {
|
||||
on bool
|
||||
all bool
|
||||
path string
|
||||
op string
|
||||
value string
|
||||
all bool
|
||||
}
|
||||
}
|
||||
|
||||
@@ -750,120 +750,27 @@ func parseArrayPath(path string) (r arrayPathResult) {
|
||||
} else if path[1] == '[' || path[1] == '(' {
|
||||
// query
|
||||
r.query.on = true
|
||||
if true {
|
||||
qpath, op, value, _, fi, ok := parseQuery(path[i:])
|
||||
if !ok {
|
||||
// bad query, end now
|
||||
break
|
||||
}
|
||||
r.query.path = qpath
|
||||
r.query.op = op
|
||||
r.query.value = value
|
||||
i = fi - 1
|
||||
if i+1 < len(path) && path[i+1] == '#' {
|
||||
r.query.all = true
|
||||
}
|
||||
} else {
|
||||
var end byte
|
||||
if path[1] == '[' {
|
||||
end = ']'
|
||||
} else {
|
||||
end = ')'
|
||||
}
|
||||
i += 2
|
||||
// whitespace
|
||||
for ; i < len(path); i++ {
|
||||
if path[i] > ' ' {
|
||||
break
|
||||
}
|
||||
}
|
||||
s := i
|
||||
for ; i < len(path); i++ {
|
||||
if path[i] <= ' ' ||
|
||||
path[i] == '!' ||
|
||||
path[i] == '=' ||
|
||||
path[i] == '<' ||
|
||||
path[i] == '>' ||
|
||||
path[i] == '%' ||
|
||||
path[i] == end {
|
||||
break
|
||||
}
|
||||
}
|
||||
r.query.path = path[s:i]
|
||||
// whitespace
|
||||
for ; i < len(path); i++ {
|
||||
if path[i] > ' ' {
|
||||
break
|
||||
}
|
||||
}
|
||||
if i < len(path) {
|
||||
s = i
|
||||
if path[i] == '!' {
|
||||
if i < len(path)-1 && (path[i+1] == '=' ||
|
||||
path[i+1] == '%') {
|
||||
i++
|
||||
}
|
||||
} else if path[i] == '<' || path[i] == '>' {
|
||||
if i < len(path)-1 && path[i+1] == '=' {
|
||||
i++
|
||||
}
|
||||
} else if path[i] == '=' {
|
||||
if i < len(path)-1 && path[i+1] == '=' {
|
||||
s++
|
||||
i++
|
||||
}
|
||||
}
|
||||
i++
|
||||
r.query.op = path[s:i]
|
||||
// whitespace
|
||||
for ; i < len(path); i++ {
|
||||
if path[i] > ' ' {
|
||||
break
|
||||
}
|
||||
}
|
||||
s = i
|
||||
for ; i < len(path); i++ {
|
||||
if path[i] == '"' {
|
||||
i++
|
||||
s2 := i
|
||||
for ; i < len(path); i++ {
|
||||
if path[i] > '\\' {
|
||||
continue
|
||||
}
|
||||
if path[i] == '"' {
|
||||
// look for an escaped slash
|
||||
if path[i-1] == '\\' {
|
||||
n := 0
|
||||
for j := i - 2; j > s2-1; j-- {
|
||||
if path[j] != '\\' {
|
||||
break
|
||||
}
|
||||
n++
|
||||
}
|
||||
if n%2 == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if path[i] == end {
|
||||
if i+1 < len(path) && path[i+1] == '#' {
|
||||
r.query.all = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if i > len(path) {
|
||||
i = len(path)
|
||||
}
|
||||
v := path[s:i]
|
||||
for len(v) > 0 && v[len(v)-1] <= ' ' {
|
||||
v = v[:len(v)-1]
|
||||
}
|
||||
r.query.value = v
|
||||
qpath, op, value, _, fi, vesc, ok :=
|
||||
parseQuery(path[i:])
|
||||
if !ok {
|
||||
// bad query, end now
|
||||
break
|
||||
}
|
||||
if len(value) > 2 && value[0] == '"' &&
|
||||
value[len(value)-1] == '"' {
|
||||
value = value[1 : len(value)-1]
|
||||
if vesc {
|
||||
value = unescape(value)
|
||||
}
|
||||
}
|
||||
r.query.path = qpath
|
||||
r.query.op = op
|
||||
r.query.value = value
|
||||
|
||||
i = fi - 1
|
||||
if i+1 < len(path) && path[i+1] == '#' {
|
||||
r.query.all = true
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
@@ -889,11 +796,11 @@ func parseArrayPath(path string) (r arrayPathResult) {
|
||||
// # middle
|
||||
// .cap # right
|
||||
func parseQuery(query string) (
|
||||
path, op, value, remain string, i int, ok bool,
|
||||
path, op, value, remain string, i int, vesc, ok bool,
|
||||
) {
|
||||
if len(query) < 2 || query[0] != '#' ||
|
||||
(query[1] != '(' && query[1] != '[') {
|
||||
return "", "", "", "", i, false
|
||||
return "", "", "", "", i, false, false
|
||||
}
|
||||
i = 2
|
||||
j := 0 // start of value part
|
||||
@@ -921,6 +828,7 @@ func parseQuery(query string) (
|
||||
i++
|
||||
for ; i < len(query); i++ {
|
||||
if query[i] == '\\' {
|
||||
vesc = true
|
||||
i++
|
||||
} else if query[i] == '"' {
|
||||
break
|
||||
@@ -929,7 +837,7 @@ func parseQuery(query string) (
|
||||
}
|
||||
}
|
||||
if depth > 0 {
|
||||
return "", "", "", "", i, false
|
||||
return "", "", "", "", i, false, false
|
||||
}
|
||||
if j > 0 {
|
||||
path = trim(query[2:j])
|
||||
@@ -966,7 +874,7 @@ func parseQuery(query string) (
|
||||
path = trim(query[2:i])
|
||||
remain = query[i+1:]
|
||||
}
|
||||
return path, op, value, remain, i + 1, true
|
||||
return path, op, value, remain, i + 1, vesc, true
|
||||
}
|
||||
|
||||
func trim(s string) string {
|
||||
@@ -1266,9 +1174,6 @@ func parseObject(c *parseContext, i int, path string) (int, bool) {
|
||||
}
|
||||
func queryMatches(rp *arrayPathResult, value Result) bool {
|
||||
rpv := rp.query.value
|
||||
if len(rpv) > 2 && rpv[0] == '"' && rpv[len(rpv)-1] == '"' {
|
||||
rpv = rpv[1 : len(rpv)-1]
|
||||
}
|
||||
if !value.Exists() {
|
||||
return false
|
||||
}
|
||||
@@ -2382,6 +2287,12 @@ func validnumber(data []byte, i int) (outi int, ok bool) {
|
||||
// sign
|
||||
if data[i] == '-' {
|
||||
i++
|
||||
if i == len(data) {
|
||||
return i, false
|
||||
}
|
||||
if data[i] < '0' || data[i] > '9' {
|
||||
return i, false
|
||||
}
|
||||
}
|
||||
// int
|
||||
if i == len(data) {
|
||||
|
||||
21
vendor/github.com/tidwall/sjson/LICENSE
generated
vendored
Normal file
21
vendor/github.com/tidwall/sjson/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Josh Baker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
277
vendor/github.com/tidwall/sjson/README.md
generated
vendored
Normal file
277
vendor/github.com/tidwall/sjson/README.md
generated
vendored
Normal file
@@ -0,0 +1,277 @@
|
||||
<p align="center">
|
||||
<img
|
||||
src="logo.png"
|
||||
width="240" height="78" border="0" alt="SJSON">
|
||||
<br>
|
||||
<a href="https://godoc.org/github.com/tidwall/sjson"><img src="https://img.shields.io/badge/api-reference-blue.svg?style=flat-square" alt="GoDoc"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">set a json value quickly</p>
|
||||
|
||||
SJSON is a Go package that provides a [very fast](#performance) and simple way to set a value in a json document.
|
||||
For quickly retrieving json values check out [GJSON](https://github.com/tidwall/gjson).
|
||||
|
||||
For a command line interface check out [JJ](https://github.com/tidwall/jj).
|
||||
|
||||
Getting Started
|
||||
===============
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
To start using SJSON, install Go and run `go get`:
|
||||
|
||||
```sh
|
||||
$ go get -u github.com/tidwall/sjson
|
||||
```
|
||||
|
||||
This will retrieve the library.
|
||||
|
||||
Set a value
|
||||
-----------
|
||||
Set sets the value for the specified path.
|
||||
A path is in dot syntax, such as "name.last" or "age".
|
||||
This function expects that the json is well-formed and validated.
|
||||
Invalid json will not panic, but it may return back unexpected results.
|
||||
Invalid paths may return an error.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "github.com/tidwall/sjson"
|
||||
|
||||
const json = `{"name":{"first":"Janet","last":"Prichard"},"age":47}`
|
||||
|
||||
func main() {
|
||||
value, _ := sjson.Set(json, "name.last", "Anderson")
|
||||
println(value)
|
||||
}
|
||||
```
|
||||
|
||||
This will print:
|
||||
|
||||
```json
|
||||
{"name":{"first":"Janet","last":"Anderson"},"age":47}
|
||||
```
|
||||
|
||||
Path syntax
|
||||
-----------
|
||||
|
||||
A path is a series of keys separated by a dot.
|
||||
The dot and colon characters can be escaped with ``\``.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": {"first": "Tom", "last": "Anderson"},
|
||||
"age":37,
|
||||
"children": ["Sara","Alex","Jack"],
|
||||
"fav.movie": "Deer Hunter",
|
||||
"friends": [
|
||||
{"first": "James", "last": "Murphy"},
|
||||
{"first": "Roger", "last": "Craig"}
|
||||
]
|
||||
}
|
||||
```
|
||||
```
|
||||
"name.last" >> "Anderson"
|
||||
"age" >> 37
|
||||
"children.1" >> "Alex"
|
||||
"friends.1.last" >> "Craig"
|
||||
```
|
||||
|
||||
The `-1` key can be used to append a value to an existing array:
|
||||
|
||||
```
|
||||
"children.-1" >> appends a new value to the end of the children array
|
||||
```
|
||||
|
||||
Normally number keys are used to modify arrays, but it's possible to force a numeric object key by using the colon character:
|
||||
|
||||
```json
|
||||
{
|
||||
"users":{
|
||||
"2313":{"name":"Sara"},
|
||||
"7839":{"name":"Andy"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A colon path would look like:
|
||||
|
||||
```
|
||||
"users.:2313.name" >> "Sara"
|
||||
```
|
||||
|
||||
Supported types
|
||||
---------------
|
||||
|
||||
Pretty much any type is supported:
|
||||
|
||||
```go
|
||||
sjson.Set(`{"key":true}`, "key", nil)
|
||||
sjson.Set(`{"key":true}`, "key", false)
|
||||
sjson.Set(`{"key":true}`, "key", 1)
|
||||
sjson.Set(`{"key":true}`, "key", 10.5)
|
||||
sjson.Set(`{"key":true}`, "key", "hello")
|
||||
sjson.Set(`{"key":true}`, "key", map[string]interface{}{"hello":"world"})
|
||||
```
|
||||
|
||||
When a type is not recognized, SJSON will fallback to the `encoding/json` Marshaller.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Set a value from empty document:
|
||||
```go
|
||||
value, _ := sjson.Set("", "name", "Tom")
|
||||
println(value)
|
||||
|
||||
// Output:
|
||||
// {"name":"Tom"}
|
||||
```
|
||||
|
||||
Set a nested value from empty document:
|
||||
```go
|
||||
value, _ := sjson.Set("", "name.last", "Anderson")
|
||||
println(value)
|
||||
|
||||
// Output:
|
||||
// {"name":{"last":"Anderson"}}
|
||||
```
|
||||
|
||||
Set a new value:
|
||||
```go
|
||||
value, _ := sjson.Set(`{"name":{"last":"Anderson"}}`, "name.first", "Sara")
|
||||
println(value)
|
||||
|
||||
// Output:
|
||||
// {"name":{"first":"Sara","last":"Anderson"}}
|
||||
```
|
||||
|
||||
Update an existing value:
|
||||
```go
|
||||
value, _ := sjson.Set(`{"name":{"last":"Anderson"}}`, "name.last", "Smith")
|
||||
println(value)
|
||||
|
||||
// Output:
|
||||
// {"name":{"last":"Smith"}}
|
||||
```
|
||||
|
||||
Set a new array value:
|
||||
```go
|
||||
value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.2", "Sara")
|
||||
println(value)
|
||||
|
||||
// Output:
|
||||
// {"friends":["Andy","Carol","Sara"]
|
||||
```
|
||||
|
||||
Append an array value by using the `-1` key in a path:
|
||||
```go
|
||||
value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.-1", "Sara")
|
||||
println(value)
|
||||
|
||||
// Output:
|
||||
// {"friends":["Andy","Carol","Sara"]
|
||||
```
|
||||
|
||||
Append an array value that is past the end:
|
||||
```go
|
||||
value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.4", "Sara")
|
||||
println(value)
|
||||
|
||||
// Output:
|
||||
// {"friends":["Andy","Carol",null,null,"Sara"]
|
||||
```
|
||||
|
||||
Delete a value:
|
||||
```go
|
||||
value, _ := sjson.Delete(`{"name":{"first":"Sara","last":"Anderson"}}`, "name.first")
|
||||
println(value)
|
||||
|
||||
// Output:
|
||||
// {"name":{"last":"Anderson"}}
|
||||
```
|
||||
|
||||
Delete an array value:
|
||||
```go
|
||||
value, _ := sjson.Delete(`{"friends":["Andy","Carol"]}`, "friends.1")
|
||||
println(value)
|
||||
|
||||
// Output:
|
||||
// {"friends":["Andy"]}
|
||||
```
|
||||
|
||||
Delete the last array value:
|
||||
```go
|
||||
value, _ := sjson.Delete(`{"friends":["Andy","Carol"]}`, "friends.-1")
|
||||
println(value)
|
||||
|
||||
// Output:
|
||||
// {"friends":["Andy"]}
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
Benchmarks of SJSON alongside [encoding/json](https://golang.org/pkg/encoding/json/),
|
||||
[ffjson](https://github.com/pquerna/ffjson),
|
||||
[EasyJSON](https://github.com/mailru/easyjson),
|
||||
and [Gabs](https://github.com/Jeffail/gabs)
|
||||
|
||||
```
|
||||
Benchmark_SJSON-8 3000000 805 ns/op 1077 B/op 3 allocs/op
|
||||
Benchmark_SJSON_ReplaceInPlace-8 3000000 449 ns/op 0 B/op 0 allocs/op
|
||||
Benchmark_JSON_Map-8 300000 21236 ns/op 6392 B/op 150 allocs/op
|
||||
Benchmark_JSON_Struct-8 300000 14691 ns/op 1789 B/op 24 allocs/op
|
||||
Benchmark_Gabs-8 300000 21311 ns/op 6752 B/op 150 allocs/op
|
||||
Benchmark_FFJSON-8 300000 17673 ns/op 3589 B/op 47 allocs/op
|
||||
Benchmark_EasyJSON-8 1500000 3119 ns/op 1061 B/op 13 allocs/op
|
||||
```
|
||||
|
||||
JSON document used:
|
||||
|
||||
```json
|
||||
{
|
||||
"widget": {
|
||||
"debug": "on",
|
||||
"window": {
|
||||
"title": "Sample Konfabulator Widget",
|
||||
"name": "main_window",
|
||||
"width": 500,
|
||||
"height": 500
|
||||
},
|
||||
"image": {
|
||||
"src": "Images/Sun.png",
|
||||
"hOffset": 250,
|
||||
"vOffset": 250,
|
||||
"alignment": "center"
|
||||
},
|
||||
"text": {
|
||||
"data": "Click Here",
|
||||
"size": 36,
|
||||
"style": "bold",
|
||||
"vOffset": 100,
|
||||
"alignment": "center",
|
||||
"onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each operation was rotated though one of the following search paths:
|
||||
|
||||
```
|
||||
widget.window.name
|
||||
widget.image.hOffset
|
||||
widget.text.onMouseUp
|
||||
```
|
||||
|
||||
*These benchmarks were run on a MacBook Pro 15" 2.8 GHz Intel Core i7 using Go 1.7 and can be be found [here](https://github.com/tidwall/sjson-benchmarks)*.
|
||||
|
||||
## Contact
|
||||
Josh Baker [@tidwall](http://twitter.com/tidwall)
|
||||
|
||||
## License
|
||||
|
||||
SJSON source code is available under the MIT [License](/LICENSE).
|
||||
8
vendor/github.com/tidwall/sjson/go.mod
generated
vendored
Normal file
8
vendor/github.com/tidwall/sjson/go.mod
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
module github.com/tidwall/sjson
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/tidwall/gjson v1.7.4
|
||||
github.com/tidwall/pretty v1.1.0
|
||||
)
|
||||
6
vendor/github.com/tidwall/sjson/go.sum
generated
vendored
Normal file
6
vendor/github.com/tidwall/sjson/go.sum
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
github.com/tidwall/gjson v1.7.4 h1:19cchw8FOxkG5mdLRkGf9jqIqEyqdZhPqW60XfyFxk8=
|
||||
github.com/tidwall/gjson v1.7.4/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=
|
||||
github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
|
||||
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.1.0 h1:K3hMW5epkdAVwibsQEfR/7Zj0Qgt4DxtNumTq/VloO8=
|
||||
github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
BIN
vendor/github.com/tidwall/sjson/logo.png
generated
vendored
Normal file
BIN
vendor/github.com/tidwall/sjson/logo.png
generated
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
669
vendor/github.com/tidwall/sjson/sjson.go
generated
vendored
Normal file
669
vendor/github.com/tidwall/sjson/sjson.go
generated
vendored
Normal file
@@ -0,0 +1,669 @@
|
||||
// Package sjson provides setting json values.
|
||||
package sjson
|
||||
|
||||
import (
|
||||
jsongo "encoding/json"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"unsafe"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type errorType struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (err *errorType) Error() string {
|
||||
return err.msg
|
||||
}
|
||||
|
||||
// Options represents additional options for the Set and Delete functions.
|
||||
type Options struct {
|
||||
// Optimistic is a hint that the value likely exists which
|
||||
// allows for the sjson to perform a fast-track search and replace.
|
||||
Optimistic bool
|
||||
// ReplaceInPlace is a hint to replace the input json rather than
|
||||
// allocate a new json byte slice. When this field is specified
|
||||
// the input json will not longer be valid and it should not be used
|
||||
// In the case when the destination slice doesn't have enough free
|
||||
// bytes to replace the data in place, a new bytes slice will be
|
||||
// created under the hood.
|
||||
// The Optimistic flag must be set to true and the input must be a
|
||||
// byte slice in order to use this field.
|
||||
ReplaceInPlace bool
|
||||
}
|
||||
|
||||
type pathResult struct {
|
||||
part string // current key part
|
||||
gpart string // gjson get part
|
||||
path string // remaining path
|
||||
force bool // force a string key
|
||||
more bool // there is more path to parse
|
||||
}
|
||||
|
||||
func parsePath(path string) (pathResult, error) {
|
||||
var r pathResult
|
||||
if len(path) > 0 && path[0] == ':' {
|
||||
r.force = true
|
||||
path = path[1:]
|
||||
}
|
||||
for i := 0; i < len(path); i++ {
|
||||
if path[i] == '.' {
|
||||
r.part = path[:i]
|
||||
r.gpart = path[:i]
|
||||
r.path = path[i+1:]
|
||||
r.more = true
|
||||
return r, nil
|
||||
}
|
||||
if path[i] == '*' || path[i] == '?' {
|
||||
return r, &errorType{"wildcard characters not allowed in path"}
|
||||
} else if path[i] == '#' {
|
||||
return r, &errorType{"array access character not allowed in path"}
|
||||
}
|
||||
if path[i] == '\\' {
|
||||
// go into escape mode. this is a slower path that
|
||||
// strips off the escape character from the part.
|
||||
epart := []byte(path[:i])
|
||||
gpart := []byte(path[:i+1])
|
||||
i++
|
||||
if i < len(path) {
|
||||
epart = append(epart, path[i])
|
||||
gpart = append(gpart, path[i])
|
||||
i++
|
||||
for ; i < len(path); i++ {
|
||||
if path[i] == '\\' {
|
||||
gpart = append(gpart, '\\')
|
||||
i++
|
||||
if i < len(path) {
|
||||
epart = append(epart, path[i])
|
||||
gpart = append(gpart, path[i])
|
||||
}
|
||||
continue
|
||||
} else if path[i] == '.' {
|
||||
r.part = string(epart)
|
||||
r.gpart = string(gpart)
|
||||
r.path = path[i+1:]
|
||||
r.more = true
|
||||
return r, nil
|
||||
} else if path[i] == '*' || path[i] == '?' {
|
||||
return r, &errorType{
|
||||
"wildcard characters not allowed in path"}
|
||||
} else if path[i] == '#' {
|
||||
return r, &errorType{
|
||||
"array access character not allowed in path"}
|
||||
}
|
||||
epart = append(epart, path[i])
|
||||
gpart = append(gpart, path[i])
|
||||
}
|
||||
}
|
||||
// append the last part
|
||||
r.part = string(epart)
|
||||
r.gpart = string(gpart)
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
r.part = path
|
||||
r.gpart = path
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func mustMarshalString(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] < ' ' || s[i] > 0x7f || s[i] == '"' || s[i] == '\\' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// appendStringify makes a json string and appends to buf.
|
||||
func appendStringify(buf []byte, s string) []byte {
|
||||
if mustMarshalString(s) {
|
||||
b, _ := jsongo.Marshal(s)
|
||||
return append(buf, b...)
|
||||
}
|
||||
buf = append(buf, '"')
|
||||
buf = append(buf, s...)
|
||||
buf = append(buf, '"')
|
||||
return buf
|
||||
}
|
||||
|
||||
// appendBuild builds a json block from a json path.
|
||||
func appendBuild(buf []byte, array bool, paths []pathResult, raw string,
|
||||
stringify bool) []byte {
|
||||
if !array {
|
||||
buf = appendStringify(buf, paths[0].part)
|
||||
buf = append(buf, ':')
|
||||
}
|
||||
if len(paths) > 1 {
|
||||
n, numeric := atoui(paths[1])
|
||||
if numeric || (!paths[1].force && paths[1].part == "-1") {
|
||||
buf = append(buf, '[')
|
||||
buf = appendRepeat(buf, "null,", n)
|
||||
buf = appendBuild(buf, true, paths[1:], raw, stringify)
|
||||
buf = append(buf, ']')
|
||||
} else {
|
||||
buf = append(buf, '{')
|
||||
buf = appendBuild(buf, false, paths[1:], raw, stringify)
|
||||
buf = append(buf, '}')
|
||||
}
|
||||
} else {
|
||||
if stringify {
|
||||
buf = appendStringify(buf, raw)
|
||||
} else {
|
||||
buf = append(buf, raw...)
|
||||
}
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
// atoui does a rip conversion of string -> unigned int.
|
||||
func atoui(r pathResult) (n int, ok bool) {
|
||||
if r.force {
|
||||
return 0, false
|
||||
}
|
||||
for i := 0; i < len(r.part); i++ {
|
||||
if r.part[i] < '0' || r.part[i] > '9' {
|
||||
return 0, false
|
||||
}
|
||||
n = n*10 + int(r.part[i]-'0')
|
||||
}
|
||||
return n, true
|
||||
}
|
||||
|
||||
// appendRepeat repeats string "n" times and appends to buf.
|
||||
func appendRepeat(buf []byte, s string, n int) []byte {
|
||||
for i := 0; i < n; i++ {
|
||||
buf = append(buf, s...)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
// trim does a rip trim
|
||||
func trim(s string) string {
|
||||
for len(s) > 0 {
|
||||
if s[0] <= ' ' {
|
||||
s = s[1:]
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
for len(s) > 0 {
|
||||
if s[len(s)-1] <= ' ' {
|
||||
s = s[:len(s)-1]
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// deleteTailItem deletes the previous key or comma.
|
||||
func deleteTailItem(buf []byte) ([]byte, bool) {
|
||||
loop:
|
||||
for i := len(buf) - 1; i >= 0; i-- {
|
||||
// look for either a ',',':','['
|
||||
switch buf[i] {
|
||||
case '[':
|
||||
return buf, true
|
||||
case ',':
|
||||
return buf[:i], false
|
||||
case ':':
|
||||
// delete tail string
|
||||
i--
|
||||
for ; i >= 0; i-- {
|
||||
if buf[i] == '"' {
|
||||
i--
|
||||
for ; i >= 0; i-- {
|
||||
if buf[i] == '"' {
|
||||
i--
|
||||
if i >= 0 && buf[i] == '\\' {
|
||||
i--
|
||||
continue
|
||||
}
|
||||
for ; i >= 0; i-- {
|
||||
// look for either a ',','{'
|
||||
switch buf[i] {
|
||||
case '{':
|
||||
return buf[:i+1], true
|
||||
case ',':
|
||||
return buf[:i], false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
break loop
|
||||
}
|
||||
}
|
||||
return buf, false
|
||||
}
|
||||
|
||||
var errNoChange = &errorType{"no change"}
|
||||
|
||||
func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string,
|
||||
stringify, del bool) ([]byte, error) {
|
||||
var err error
|
||||
var res gjson.Result
|
||||
var found bool
|
||||
if del {
|
||||
if paths[0].part == "-1" && !paths[0].force {
|
||||
res = gjson.Get(jstr, "#")
|
||||
if res.Int() > 0 {
|
||||
res = gjson.Get(jstr, strconv.FormatInt(int64(res.Int()-1), 10))
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
res = gjson.Get(jstr, paths[0].gpart)
|
||||
}
|
||||
if res.Index > 0 {
|
||||
if len(paths) > 1 {
|
||||
buf = append(buf, jstr[:res.Index]...)
|
||||
buf, err = appendRawPaths(buf, res.Raw, paths[1:], raw,
|
||||
stringify, del)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf = append(buf, jstr[res.Index+len(res.Raw):]...)
|
||||
return buf, nil
|
||||
}
|
||||
buf = append(buf, jstr[:res.Index]...)
|
||||
var exidx int // additional forward stripping
|
||||
if del {
|
||||
var delNextComma bool
|
||||
buf, delNextComma = deleteTailItem(buf)
|
||||
if delNextComma {
|
||||
i, j := res.Index+len(res.Raw), 0
|
||||
for ; i < len(jstr); i, j = i+1, j+1 {
|
||||
if jstr[i] <= ' ' {
|
||||
continue
|
||||
}
|
||||
if jstr[i] == ',' {
|
||||
exidx = j + 1
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if stringify {
|
||||
buf = appendStringify(buf, raw)
|
||||
} else {
|
||||
buf = append(buf, raw...)
|
||||
}
|
||||
}
|
||||
buf = append(buf, jstr[res.Index+len(res.Raw)+exidx:]...)
|
||||
return buf, nil
|
||||
}
|
||||
if del {
|
||||
return nil, errNoChange
|
||||
}
|
||||
n, numeric := atoui(paths[0])
|
||||
isempty := true
|
||||
for i := 0; i < len(jstr); i++ {
|
||||
if jstr[i] > ' ' {
|
||||
isempty = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if isempty {
|
||||
if numeric {
|
||||
jstr = "[]"
|
||||
} else {
|
||||
jstr = "{}"
|
||||
}
|
||||
}
|
||||
jsres := gjson.Parse(jstr)
|
||||
if jsres.Type != gjson.JSON {
|
||||
if numeric {
|
||||
jstr = "[]"
|
||||
} else {
|
||||
jstr = "{}"
|
||||
}
|
||||
jsres = gjson.Parse(jstr)
|
||||
}
|
||||
var comma bool
|
||||
for i := 1; i < len(jsres.Raw); i++ {
|
||||
if jsres.Raw[i] <= ' ' {
|
||||
continue
|
||||
}
|
||||
if jsres.Raw[i] == '}' || jsres.Raw[i] == ']' {
|
||||
break
|
||||
}
|
||||
comma = true
|
||||
break
|
||||
}
|
||||
switch jsres.Raw[0] {
|
||||
default:
|
||||
return nil, &errorType{"json must be an object or array"}
|
||||
case '{':
|
||||
end := len(jsres.Raw) - 1
|
||||
for ; end > 0; end-- {
|
||||
if jsres.Raw[end] == '}' {
|
||||
break
|
||||
}
|
||||
}
|
||||
buf = append(buf, jsres.Raw[:end]...)
|
||||
if comma {
|
||||
buf = append(buf, ',')
|
||||
}
|
||||
buf = appendBuild(buf, false, paths, raw, stringify)
|
||||
buf = append(buf, '}')
|
||||
return buf, nil
|
||||
case '[':
|
||||
var appendit bool
|
||||
if !numeric {
|
||||
if paths[0].part == "-1" && !paths[0].force {
|
||||
appendit = true
|
||||
} else {
|
||||
return nil, &errorType{
|
||||
"cannot set array element for non-numeric key '" +
|
||||
paths[0].part + "'"}
|
||||
}
|
||||
}
|
||||
if appendit {
|
||||
njson := trim(jsres.Raw)
|
||||
if njson[len(njson)-1] == ']' {
|
||||
njson = njson[:len(njson)-1]
|
||||
}
|
||||
buf = append(buf, njson...)
|
||||
if comma {
|
||||
buf = append(buf, ',')
|
||||
}
|
||||
|
||||
buf = appendBuild(buf, true, paths, raw, stringify)
|
||||
buf = append(buf, ']')
|
||||
return buf, nil
|
||||
}
|
||||
buf = append(buf, '[')
|
||||
ress := jsres.Array()
|
||||
for i := 0; i < len(ress); i++ {
|
||||
if i > 0 {
|
||||
buf = append(buf, ',')
|
||||
}
|
||||
buf = append(buf, ress[i].Raw...)
|
||||
}
|
||||
if len(ress) == 0 {
|
||||
buf = appendRepeat(buf, "null,", n-len(ress))
|
||||
} else {
|
||||
buf = appendRepeat(buf, ",null", n-len(ress))
|
||||
if comma {
|
||||
buf = append(buf, ',')
|
||||
}
|
||||
}
|
||||
buf = appendBuild(buf, true, paths, raw, stringify)
|
||||
buf = append(buf, ']')
|
||||
return buf, nil
|
||||
}
|
||||
}
|
||||
|
||||
func isOptimisticPath(path string) bool {
|
||||
for i := 0; i < len(path); i++ {
|
||||
if path[i] < '.' || path[i] > 'z' {
|
||||
return false
|
||||
}
|
||||
if path[i] > '9' && path[i] < 'A' {
|
||||
return false
|
||||
}
|
||||
if path[i] > 'z' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Set sets a json value for the specified path.
|
||||
// A path is in dot syntax, such as "name.last" or "age".
|
||||
// This function expects that the json is well-formed, and does not validate.
|
||||
// Invalid json will not panic, but it may return back unexpected results.
|
||||
// An error is returned if the path is not valid.
|
||||
//
|
||||
// A path is a series of keys separated by a dot.
|
||||
//
|
||||
// {
|
||||
// "name": {"first": "Tom", "last": "Anderson"},
|
||||
// "age":37,
|
||||
// "children": ["Sara","Alex","Jack"],
|
||||
// "friends": [
|
||||
// {"first": "James", "last": "Murphy"},
|
||||
// {"first": "Roger", "last": "Craig"}
|
||||
// ]
|
||||
// }
|
||||
// "name.last" >> "Anderson"
|
||||
// "age" >> 37
|
||||
// "children.1" >> "Alex"
|
||||
//
|
||||
func Set(json, path string, value interface{}) (string, error) {
|
||||
return SetOptions(json, path, value, nil)
|
||||
}
|
||||
|
||||
// SetBytes sets a json value for the specified path.
|
||||
// If working with bytes, this method preferred over
|
||||
// Set(string(data), path, value)
|
||||
func SetBytes(json []byte, path string, value interface{}) ([]byte, error) {
|
||||
return SetBytesOptions(json, path, value, nil)
|
||||
}
|
||||
|
||||
// SetRaw sets a raw json value for the specified path.
|
||||
// This function works the same as Set except that the value is set as a
|
||||
// raw block of json. This allows for setting premarshalled json objects.
|
||||
func SetRaw(json, path, value string) (string, error) {
|
||||
return SetRawOptions(json, path, value, nil)
|
||||
}
|
||||
|
||||
// SetRawOptions sets a raw json value for the specified path with options.
|
||||
// This furnction works the same as SetOptions except that the value is set
|
||||
// as a raw block of json. This allows for setting premarshalled json objects.
|
||||
func SetRawOptions(json, path, value string, opts *Options) (string, error) {
|
||||
var optimistic bool
|
||||
if opts != nil {
|
||||
optimistic = opts.Optimistic
|
||||
}
|
||||
res, err := set(json, path, value, false, false, optimistic, false)
|
||||
if err == errNoChange {
|
||||
return json, nil
|
||||
}
|
||||
return string(res), err
|
||||
}
|
||||
|
||||
// SetRawBytes sets a raw json value for the specified path.
|
||||
// If working with bytes, this method preferred over
|
||||
// SetRaw(string(data), path, value)
|
||||
func SetRawBytes(json []byte, path string, value []byte) ([]byte, error) {
|
||||
return SetRawBytesOptions(json, path, value, nil)
|
||||
}
|
||||
|
||||
type dtype struct{}
|
||||
|
||||
// Delete deletes a value from json for the specified path.
|
||||
func Delete(json, path string) (string, error) {
|
||||
return Set(json, path, dtype{})
|
||||
}
|
||||
|
||||
// DeleteBytes deletes a value from json for the specified path.
|
||||
func DeleteBytes(json []byte, path string) ([]byte, error) {
|
||||
return SetBytes(json, path, dtype{})
|
||||
}
|
||||
|
||||
func set(jstr, path, raw string,
|
||||
stringify, del, optimistic, inplace bool) ([]byte, error) {
|
||||
if path == "" {
|
||||
return nil, &errorType{"path cannot be empty"}
|
||||
}
|
||||
if !del && optimistic && isOptimisticPath(path) {
|
||||
res := gjson.Get(jstr, path)
|
||||
if res.Exists() && res.Index > 0 {
|
||||
sz := len(jstr) - len(res.Raw) + len(raw)
|
||||
if stringify {
|
||||
sz += 2
|
||||
}
|
||||
if inplace && sz <= len(jstr) {
|
||||
if !stringify || !mustMarshalString(raw) {
|
||||
jsonh := *(*reflect.StringHeader)(unsafe.Pointer(&jstr))
|
||||
jsonbh := reflect.SliceHeader{
|
||||
Data: jsonh.Data, Len: jsonh.Len, Cap: jsonh.Len}
|
||||
jbytes := *(*[]byte)(unsafe.Pointer(&jsonbh))
|
||||
if stringify {
|
||||
jbytes[res.Index] = '"'
|
||||
copy(jbytes[res.Index+1:], []byte(raw))
|
||||
jbytes[res.Index+1+len(raw)] = '"'
|
||||
copy(jbytes[res.Index+1+len(raw)+1:],
|
||||
jbytes[res.Index+len(res.Raw):])
|
||||
} else {
|
||||
copy(jbytes[res.Index:], []byte(raw))
|
||||
copy(jbytes[res.Index+len(raw):],
|
||||
jbytes[res.Index+len(res.Raw):])
|
||||
}
|
||||
return jbytes[:sz], nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
buf := make([]byte, 0, sz)
|
||||
buf = append(buf, jstr[:res.Index]...)
|
||||
if stringify {
|
||||
buf = appendStringify(buf, raw)
|
||||
} else {
|
||||
buf = append(buf, raw...)
|
||||
}
|
||||
buf = append(buf, jstr[res.Index+len(res.Raw):]...)
|
||||
return buf, nil
|
||||
}
|
||||
}
|
||||
// parse the path, make sure that it does not contain invalid characters
|
||||
// such as '#', '?', '*'
|
||||
paths := make([]pathResult, 0, 4)
|
||||
r, err := parsePath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paths = append(paths, r)
|
||||
for r.more {
|
||||
if r, err = parsePath(r.path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paths = append(paths, r)
|
||||
}
|
||||
|
||||
njson, err := appendRawPaths(nil, jstr, paths, raw, stringify, del)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return njson, nil
|
||||
}
|
||||
|
||||
// SetOptions sets a json value for the specified path with options.
|
||||
// A path is in dot syntax, such as "name.last" or "age".
|
||||
// This function expects that the json is well-formed, and does not validate.
|
||||
// Invalid json will not panic, but it may return back unexpected results.
|
||||
// An error is returned if the path is not valid.
|
||||
func SetOptions(json, path string, value interface{},
|
||||
opts *Options) (string, error) {
|
||||
if opts != nil {
|
||||
if opts.ReplaceInPlace {
|
||||
// it's not safe to replace bytes in-place for strings
|
||||
// copy the Options and set options.ReplaceInPlace to false.
|
||||
nopts := *opts
|
||||
opts = &nopts
|
||||
opts.ReplaceInPlace = false
|
||||
}
|
||||
}
|
||||
jsonh := *(*reflect.StringHeader)(unsafe.Pointer(&json))
|
||||
jsonbh := reflect.SliceHeader{Data: jsonh.Data, Len: jsonh.Len}
|
||||
jsonb := *(*[]byte)(unsafe.Pointer(&jsonbh))
|
||||
res, err := SetBytesOptions(jsonb, path, value, opts)
|
||||
return string(res), err
|
||||
}
|
||||
|
||||
// SetBytesOptions sets a json value for the specified path with options.
|
||||
// If working with bytes, this method preferred over
|
||||
// SetOptions(string(data), path, value)
|
||||
func SetBytesOptions(json []byte, path string, value interface{},
|
||||
opts *Options) ([]byte, error) {
|
||||
var optimistic, inplace bool
|
||||
if opts != nil {
|
||||
optimistic = opts.Optimistic
|
||||
inplace = opts.ReplaceInPlace
|
||||
}
|
||||
jstr := *(*string)(unsafe.Pointer(&json))
|
||||
var res []byte
|
||||
var err error
|
||||
switch v := value.(type) {
|
||||
default:
|
||||
b, merr := jsongo.Marshal(value)
|
||||
if merr != nil {
|
||||
return nil, merr
|
||||
}
|
||||
raw := *(*string)(unsafe.Pointer(&b))
|
||||
res, err = set(jstr, path, raw, false, false, optimistic, inplace)
|
||||
case dtype:
|
||||
res, err = set(jstr, path, "", false, true, optimistic, inplace)
|
||||
case string:
|
||||
res, err = set(jstr, path, v, true, false, optimistic, inplace)
|
||||
case []byte:
|
||||
raw := *(*string)(unsafe.Pointer(&v))
|
||||
res, err = set(jstr, path, raw, true, false, optimistic, inplace)
|
||||
case bool:
|
||||
if v {
|
||||
res, err = set(jstr, path, "true", false, false, optimistic, inplace)
|
||||
} else {
|
||||
res, err = set(jstr, path, "false", false, false, optimistic, inplace)
|
||||
}
|
||||
case int8:
|
||||
res, err = set(jstr, path, strconv.FormatInt(int64(v), 10),
|
||||
false, false, optimistic, inplace)
|
||||
case int16:
|
||||
res, err = set(jstr, path, strconv.FormatInt(int64(v), 10),
|
||||
false, false, optimistic, inplace)
|
||||
case int32:
|
||||
res, err = set(jstr, path, strconv.FormatInt(int64(v), 10),
|
||||
false, false, optimistic, inplace)
|
||||
case int64:
|
||||
res, err = set(jstr, path, strconv.FormatInt(int64(v), 10),
|
||||
false, false, optimistic, inplace)
|
||||
case uint8:
|
||||
res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10),
|
||||
false, false, optimistic, inplace)
|
||||
case uint16:
|
||||
res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10),
|
||||
false, false, optimistic, inplace)
|
||||
case uint32:
|
||||
res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10),
|
||||
false, false, optimistic, inplace)
|
||||
case uint64:
|
||||
res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10),
|
||||
false, false, optimistic, inplace)
|
||||
case float32:
|
||||
res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64),
|
||||
false, false, optimistic, inplace)
|
||||
case float64:
|
||||
res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64),
|
||||
false, false, optimistic, inplace)
|
||||
}
|
||||
if err == errNoChange {
|
||||
return json, nil
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
// SetRawBytesOptions sets a raw json value for the specified path with options.
|
||||
// If working with bytes, this method preferred over
|
||||
// SetRawOptions(string(data), path, value, opts)
|
||||
func SetRawBytesOptions(json []byte, path string, value []byte,
|
||||
opts *Options) ([]byte, error) {
|
||||
jstr := *(*string)(unsafe.Pointer(&json))
|
||||
vstr := *(*string)(unsafe.Pointer(&value))
|
||||
var optimistic, inplace bool
|
||||
if opts != nil {
|
||||
optimistic = opts.Optimistic
|
||||
inplace = opts.ReplaceInPlace
|
||||
}
|
||||
res, err := set(jstr, path, vstr, false, false, optimistic, inplace)
|
||||
if err == errNoChange {
|
||||
return json, nil
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
4
vendor/modules.txt
vendored
4
vendor/modules.txt
vendored
@@ -556,12 +556,14 @@ github.com/spf13/pflag
|
||||
# github.com/stretchr/testify v1.7.0
|
||||
github.com/stretchr/testify/assert
|
||||
github.com/stretchr/testify/require
|
||||
# github.com/tidwall/gjson v1.7.3
|
||||
# github.com/tidwall/gjson v1.7.5
|
||||
github.com/tidwall/gjson
|
||||
# github.com/tidwall/match v1.0.3
|
||||
github.com/tidwall/match
|
||||
# github.com/tidwall/pretty v1.1.0
|
||||
github.com/tidwall/pretty
|
||||
# github.com/tidwall/sjson v1.1.6
|
||||
github.com/tidwall/sjson
|
||||
# github.com/xanzy/ssh-agent v0.2.1
|
||||
github.com/xanzy/ssh-agent
|
||||
# github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb
|
||||
|
||||
Reference in New Issue
Block a user