// Copyright (c) 2017 Tigera, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package names import ( "errors" "fmt" "reflect" "strings" cerrors "github.com/projectcalico/libcalico-go/lib/errors" ) // WorkloadEndpointIdentifiers is a collection of identifiers that are used to uniquely // identify a WorkloadEndpoint resource. Since a resource is identified by a single // name field, Calico requires the name to be constructed in a very specific format. // The format is dependent on the Orchestrator type: // - k8s: -k8s-- // - cni: -cni-- // - libnetwork: -libnetwork-libnetwork- // - (other): --- // // Each parameter cannot start or end with a dash (-), and dashes within the parameter // will be escaped to a double-dash (--) in the constructed name. // // List queries allow for prefix lists (for non-KDD), the client should verify that // the items returned in the list match the supplied identifiers using the // NameMatches() method. This is necessary because a prefix match may return endpoints // that do not exactly match the required identifiers. For example, suppose you are // querying endpoints with node=node1, orch=k8s, pod=pod and endpoints is wild carded: // - The name prefix would be `node1-k8s-pod-` // - A list query using that prefix would also return endpoints with, for example, // a pod call "pod-1", because the name of the endpoint might be `node1-k8s-pod--1-eth0` // which matches the required name prefix. // // The Node and Orchestrator are always required for both prefix and non-prefix name // construction. type WorkloadEndpointIdentifiers struct { Node string Orchestrator string Endpoint string Workload string Pod string ContainerID string } // NameMatches returns true if the supplied WorkloadEndpoint name matches the // supplied identifiers. // This will return an error if the identifiers are not valid. func (ids WorkloadEndpointIdentifiers) NameMatches(name string) (bool, error) { // Extract the required segments for this orchestrator type. req, err := ids.getSegments() if err != nil { return false, err } // Extract the parameters from the name. parts := ExtractDashSeparatedParms(name, len(req)) if len(parts) == 0 { return false, nil } // Check each name segment for a non-match. for i, r := range req { if r.value != "" && r.value != parts[i] { return false, nil } } return true, nil } // CalculateWorkloadEndpointName calculates the expected name for a workload // endpoint given the supplied Spec. Calico requires a precise naming convention // for workload endpoints that is based on orchestrator and various other orchestrator // specific parameters. // // If allowPrefix is true, we construct the name prefix up to the last specified index // and terminate with a dash. func (ids WorkloadEndpointIdentifiers) CalculateWorkloadEndpointName(allowPrefix bool) (string, error) { req, err := ids.getSegments() if err != nil { return "", err } parts := []string{} for _, s := range req { part := "" if len(s.value) == 0 { // This segment has no value associated with it. if !allowPrefix { // We are not allowing prefixes. This is an error scenario return "", cerrors.ErrorValidation{ ErroredFields: []cerrors.ErroredField{ {Name: s.field, Value: s.value, Reason: "field should be assigned"}, }, } } // We are allowing prefixes, so return the prefix that we have constructed thus far, // terminating with a "-". return strings.Join(parts, "-") + "-", nil } part, ef := escapeDashes(s) if ef != nil { return "", cerrors.ErrorValidation{ErroredFields: []cerrors.ErroredField{*ef}} } parts = append(parts, part) } // We have extracted all of the required segments, join the segments with a "-" and // return that as the name. return strings.Join(parts, "-"), nil } // getSegments returns the ID segments specific to the orchestrator. func (ids WorkloadEndpointIdentifiers) getSegments() ([]segment, error) { node := segment{value: ids.Node, field: "node", structField: "Node"} orch := segment{value: ids.Orchestrator, field: "orchestrator", structField: "Orchestrator"} cont := segment{value: ids.ContainerID, field: "containerID", structField: "ContainerID"} pod := segment{value: ids.Pod, field: "pod", structField: "Pod"} endp := segment{value: ids.Endpoint, field: "endpoint", structField: "Endpoint"} workl := segment{value: ids.Workload, field: "workload", structField: "Workload"} // Node is *always* required. if len(node.value) == 0 { return nil, cerrors.ErrorValidation{ ErroredFields: []cerrors.ErroredField{ {Name: node.field, Reason: "field should be assigned"}, }, } } // Extract the segment values based on the orchestrator. var segments []segment switch orch.value { case "k8s": segments = []segment{node, orch, pod, endp} case "cni": segments = []segment{node, orch, cont, endp} case "libnetwork": segments = []segment{node, orch, orch, endp} default: segments = []segment{node, orch, workl, endp} } return segments, nil } // Segment contains the information of a single name segment. The field names // are geared towards the struct definition of the corresponding resource. type segment struct { // The value of the field. value string // The JSON/YAML name of the corresponding field in the WorkloadEndpointSpec field string // The structure name of the corresponding field in the WorkloadEndpointSpec structField string } // escapeDashes replaces a single dash with a double dash. This type of escaping is // used for names constructed by joining a set of names with dashes - it assumes that // each name segment cannot begin or end in a dash. func escapeDashes(seg segment) (string, *cerrors.ErroredField) { if seg.value[0] == '-' { return "", &cerrors.ErroredField{Name: seg.field, Value: seg.value, Reason: "field must not begin with a '-'"} } if seg.value[len(seg.value)-1] == '-' { return "", &cerrors.ErroredField{Name: seg.field, Value: seg.value, Reason: "field must not end with a '-'"} } return strings.Replace(seg.value, "-", "--", -1), nil } func extractParts(name string) []string { parts := []string{} lastDash := -1 for i := 1; i < len(name); i++ { // Skip non-dashes. if name[i] != '-' { continue } // Skip over double dashes if i < len(name)-1 && name[i+1] == '-' { i++ continue } // This is a dash separator. parts = append(parts, strings.Replace(name[lastDash+1:i], "--", "-", -1)) lastDash = i } // Add the last segment. parts = append(parts, strings.Replace(name[lastDash+1:], "--", "-", -1)) return parts } // Extract the dash separated parms from the name. Each parm will have had their dashes escaped, // this also removes that escaping. Returns nil if the parameters could not be extracted. func ExtractDashSeparatedParms(name string, numParms int) []string { // The name must be at least as long as the number of parameters plus the separators. if len(name) < (2*numParms - 1) { return nil } parts := extractParts(name) // We should have extracted the correct number of name segments. if len(parts) != numParms { return nil } return parts } var ( k8sFields = []string{"Pod", "Endpoint"} cniFields = []string{"ContainerID", "Endpoint"} libnetworkFields = []string{"Orchestrator", "Endpoint"} otherFields = []string{"Workload", "Endpoint"} ) // ParseWorkloadEndpointName parses a given name and returns a WorkloadEndpointIdentifiers // instance with fields populated according to the WorkloadEndpoint name format. func ParseWorkloadEndpointName(wepName string) (WorkloadEndpointIdentifiers, error) { if len(wepName) == 0 { return WorkloadEndpointIdentifiers{}, errors.New("Cannot parse emty string") } parts := extractParts(wepName) if parts == nil || len(parts) == 0 { return WorkloadEndpointIdentifiers{}, fmt.Errorf("Cannot parse %s", wepName) } pl := len(parts) weid := WorkloadEndpointIdentifiers{Node: parts[0]} if pl > 1 { weid.Orchestrator = parts[1] var orchFlds []string switch parts[1] { case "k8s": orchFlds = k8sFields case "cni": orchFlds = cniFields case "libnetwork": orchFlds = libnetworkFields default: orchFlds = otherFields } if pl > 2 { weidR := reflect.ValueOf(&weid) weidStruct := weidR.Elem() for i, part := range parts[2:] { fld := weidStruct.FieldByName(orchFlds[i]) fld.SetString(part) } } } return weid, nil }