2018-02-28 19:36:54 +00:00
|
|
|
package agent
|
|
|
|
|
|
|
|
import (
|
2018-02-28 22:02:00 +00:00
|
|
|
"fmt"
|
2018-02-28 19:36:54 +00:00
|
|
|
"net/http"
|
2018-02-28 23:54:48 +00:00
|
|
|
"strings"
|
2018-02-28 19:36:54 +00:00
|
|
|
|
2018-02-28 23:54:48 +00:00
|
|
|
"github.com/hashicorp/consul/agent/consul"
|
2018-02-28 19:36:54 +00:00
|
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
|
|
)
|
|
|
|
|
2020-06-26 21:59:15 +00:00
|
|
|
// /v1/connect/intentions
|
2020-09-04 18:42:15 +00:00
|
|
|
func (s *HTTPHandlers) IntentionEndpoint(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2018-02-28 22:02:00 +00:00
|
|
|
switch req.Method {
|
|
|
|
case "GET":
|
|
|
|
return s.IntentionList(resp, req)
|
|
|
|
|
|
|
|
case "POST":
|
|
|
|
return s.IntentionCreate(resp, req)
|
|
|
|
|
|
|
|
default:
|
|
|
|
return nil, MethodNotAllowedError{req.Method, []string{"GET", "POST"}}
|
2018-02-28 19:36:54 +00:00
|
|
|
}
|
2018-02-28 22:02:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// GET /v1/connect/intentions
|
2020-09-04 18:42:15 +00:00
|
|
|
func (s *HTTPHandlers) IntentionList(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2018-02-28 22:02:00 +00:00
|
|
|
// Method is tested in IntentionEndpoint
|
2018-02-28 19:36:54 +00:00
|
|
|
|
|
|
|
var args structs.DCSpecificRequest
|
|
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2020-06-26 21:59:15 +00:00
|
|
|
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-02-28 19:36:54 +00:00
|
|
|
var reply structs.IndexedIntentions
|
2019-02-19 11:45:36 +00:00
|
|
|
defer setMeta(resp, &reply.QueryMeta)
|
2018-02-28 19:36:54 +00:00
|
|
|
if err := s.agent.RPC("Intention.List", &args, &reply); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return reply.Intentions, nil
|
|
|
|
}
|
2018-02-28 22:02:00 +00:00
|
|
|
|
|
|
|
// POST /v1/connect/intentions
|
2020-09-04 18:42:15 +00:00
|
|
|
func (s *HTTPHandlers) IntentionCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2018-02-28 22:02:00 +00:00
|
|
|
// Method is tested in IntentionEndpoint
|
|
|
|
|
2020-06-26 21:59:15 +00:00
|
|
|
var entMeta structs.EnterpriseMeta
|
|
|
|
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-02-28 22:02:00 +00:00
|
|
|
args := structs.IntentionRequest{
|
|
|
|
Op: structs.IntentionOpCreate,
|
|
|
|
}
|
|
|
|
s.parseDC(req, &args.Datacenter)
|
|
|
|
s.parseToken(req, &args.Token)
|
2019-10-29 18:13:36 +00:00
|
|
|
if err := decodeBody(req.Body, &args.Intention); err != nil {
|
2018-06-05 18:09:45 +00:00
|
|
|
return nil, fmt.Errorf("Failed to decode request body: %s", err)
|
2018-02-28 22:02:00 +00:00
|
|
|
}
|
|
|
|
|
2020-06-26 21:59:15 +00:00
|
|
|
args.Intention.FillNonDefaultNamespaces(&entMeta)
|
|
|
|
|
|
|
|
if err := s.validateEnterpriseIntention(args.Intention); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-02-28 22:02:00 +00:00
|
|
|
var reply string
|
|
|
|
if err := s.agent.RPC("Intention.Apply", &args, &reply); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return intentionCreateResponse{reply}, nil
|
|
|
|
}
|
|
|
|
|
2020-09-04 18:42:15 +00:00
|
|
|
func (s *HTTPHandlers) validateEnterpriseIntention(ixn *structs.Intention) error {
|
2020-06-26 21:59:15 +00:00
|
|
|
if err := s.validateEnterpriseIntentionNamespace("SourceNS", ixn.SourceNS, true); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := s.validateEnterpriseIntentionNamespace("DestinationNS", ixn.DestinationNS, true); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-03-02 22:17:21 +00:00
|
|
|
// GET /v1/connect/intentions/match
|
2020-09-04 18:42:15 +00:00
|
|
|
func (s *HTTPHandlers) IntentionMatch(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2018-03-02 22:17:21 +00:00
|
|
|
// Prepare args
|
|
|
|
args := &structs.IntentionQueryRequest{Match: &structs.IntentionQueryMatch{}}
|
|
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2020-06-26 21:59:15 +00:00
|
|
|
var entMeta structs.EnterpriseMeta
|
|
|
|
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-03-02 22:17:21 +00:00
|
|
|
q := req.URL.Query()
|
|
|
|
|
|
|
|
// Extract the "by" query parameter
|
|
|
|
if by, ok := q["by"]; !ok || len(by) != 1 {
|
|
|
|
return nil, fmt.Errorf("required query parameter 'by' not set")
|
|
|
|
} else {
|
|
|
|
switch v := structs.IntentionMatchType(by[0]); v {
|
|
|
|
case structs.IntentionMatchSource, structs.IntentionMatchDestination:
|
|
|
|
args.Match.Type = v
|
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("'by' parameter must be one of 'source' or 'destination'")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Extract all the match names
|
|
|
|
names, ok := q["name"]
|
|
|
|
if !ok || len(names) == 0 {
|
|
|
|
return nil, fmt.Errorf("required query parameter 'name' not set")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Build the entries in order. The order matters since that is the
|
|
|
|
// order of the returned responses.
|
|
|
|
args.Match.Entries = make([]structs.IntentionMatchEntry, len(names))
|
|
|
|
for i, n := range names {
|
2020-06-26 21:59:15 +00:00
|
|
|
entry, err := parseIntentionMatchEntry(n, &entMeta)
|
2018-03-02 22:17:21 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("name %q is invalid: %s", n, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
args.Match.Entries[i] = entry
|
|
|
|
}
|
|
|
|
|
|
|
|
var reply structs.IndexedIntentionMatches
|
|
|
|
if err := s.agent.RPC("Intention.Match", args, &reply); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// We must have an identical count of matches
|
|
|
|
if len(reply.Matches) != len(names) {
|
|
|
|
return nil, fmt.Errorf("internal error: match response count didn't match input count")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Use empty list instead of nil.
|
|
|
|
response := make(map[string]structs.Intentions)
|
|
|
|
for i, ixns := range reply.Matches {
|
|
|
|
response[names[i]] = ixns
|
|
|
|
}
|
|
|
|
|
|
|
|
return response, nil
|
|
|
|
}
|
|
|
|
|
2018-05-19 04:03:10 +00:00
|
|
|
// GET /v1/connect/intentions/check
|
2020-09-04 18:42:15 +00:00
|
|
|
func (s *HTTPHandlers) IntentionCheck(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2018-05-11 05:52:57 +00:00
|
|
|
// Prepare args
|
2018-05-11 16:19:22 +00:00
|
|
|
args := &structs.IntentionQueryRequest{Check: &structs.IntentionQueryCheck{}}
|
2018-05-11 05:52:57 +00:00
|
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2020-06-26 21:59:15 +00:00
|
|
|
var entMeta structs.EnterpriseMeta
|
|
|
|
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-05-11 05:52:57 +00:00
|
|
|
q := req.URL.Query()
|
|
|
|
|
|
|
|
// Set the source type if set
|
2018-05-11 16:19:22 +00:00
|
|
|
args.Check.SourceType = structs.IntentionSourceConsul
|
2018-05-11 05:52:57 +00:00
|
|
|
if sourceType, ok := q["source-type"]; ok && len(sourceType) > 0 {
|
2018-05-11 16:19:22 +00:00
|
|
|
args.Check.SourceType = structs.IntentionSourceType(sourceType[0])
|
2018-05-11 05:52:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Extract the source/destination
|
|
|
|
source, ok := q["source"]
|
|
|
|
if !ok || len(source) != 1 {
|
|
|
|
return nil, fmt.Errorf("required query parameter 'source' not set")
|
|
|
|
}
|
|
|
|
destination, ok := q["destination"]
|
|
|
|
if !ok || len(destination) != 1 {
|
|
|
|
return nil, fmt.Errorf("required query parameter 'destination' not set")
|
|
|
|
}
|
|
|
|
|
|
|
|
// We parse them the same way as matches to extract namespace/name
|
2018-05-11 16:19:22 +00:00
|
|
|
args.Check.SourceName = source[0]
|
|
|
|
if args.Check.SourceType == structs.IntentionSourceConsul {
|
2020-06-26 21:59:15 +00:00
|
|
|
entry, err := parseIntentionMatchEntry(source[0], &entMeta)
|
2018-05-11 05:52:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("source %q is invalid: %s", source[0], err)
|
|
|
|
}
|
2018-05-11 16:19:22 +00:00
|
|
|
args.Check.SourceNS = entry.Namespace
|
|
|
|
args.Check.SourceName = entry.Name
|
2018-05-11 05:52:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// The destination is always in the Consul format
|
2020-06-26 21:59:15 +00:00
|
|
|
entry, err := parseIntentionMatchEntry(destination[0], &entMeta)
|
2018-05-11 05:52:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err)
|
|
|
|
}
|
2018-05-11 16:19:22 +00:00
|
|
|
args.Check.DestinationNS = entry.Namespace
|
|
|
|
args.Check.DestinationName = entry.Name
|
2018-05-11 05:52:57 +00:00
|
|
|
|
2018-05-11 16:19:22 +00:00
|
|
|
var reply structs.IntentionQueryCheckResponse
|
|
|
|
if err := s.agent.RPC("Intention.Check", args, &reply); err != nil {
|
2018-05-11 05:52:57 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &reply, nil
|
|
|
|
}
|
|
|
|
|
2020-06-26 21:59:15 +00:00
|
|
|
// GET /v1/connect/intentions/exact
|
2020-09-04 18:42:15 +00:00
|
|
|
func (s *HTTPHandlers) IntentionGetExact(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2020-06-26 21:59:15 +00:00
|
|
|
var entMeta structs.EnterpriseMeta
|
|
|
|
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
args := structs.IntentionQueryRequest{
|
|
|
|
Exact: &structs.IntentionQueryExact{},
|
|
|
|
}
|
|
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
q := req.URL.Query()
|
|
|
|
|
|
|
|
// Extract the source/destination
|
|
|
|
source, ok := q["source"]
|
|
|
|
if !ok || len(source) != 1 {
|
|
|
|
return nil, fmt.Errorf("required query parameter 'source' not set")
|
|
|
|
}
|
|
|
|
destination, ok := q["destination"]
|
|
|
|
if !ok || len(destination) != 1 {
|
|
|
|
return nil, fmt.Errorf("required query parameter 'destination' not set")
|
|
|
|
}
|
|
|
|
|
|
|
|
{
|
|
|
|
entry, err := parseIntentionMatchEntry(source[0], &entMeta)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("source %q is invalid: %s", source[0], err)
|
|
|
|
}
|
|
|
|
args.Exact.SourceNS = entry.Namespace
|
|
|
|
args.Exact.SourceName = entry.Name
|
|
|
|
}
|
|
|
|
|
|
|
|
{
|
|
|
|
entry, err := parseIntentionMatchEntry(destination[0], &entMeta)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err)
|
|
|
|
}
|
|
|
|
args.Exact.DestinationNS = entry.Namespace
|
|
|
|
args.Exact.DestinationName = entry.Name
|
|
|
|
}
|
|
|
|
|
|
|
|
var reply structs.IndexedIntentions
|
|
|
|
if err := s.agent.RPC("Intention.Get", &args, &reply); err != nil {
|
|
|
|
// We have to check the string since the RPC sheds the error type
|
|
|
|
if err.Error() == consul.ErrIntentionNotFound.Error() {
|
|
|
|
resp.WriteHeader(http.StatusNotFound)
|
|
|
|
fmt.Fprint(resp, err.Error())
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Not ideal, but there are a number of error scenarios that are not
|
|
|
|
// user error (400). We look for a specific case of invalid UUID
|
|
|
|
// to detect a parameter error and return a 400 response. The error
|
|
|
|
// is not a constant type or message, so we have to use strings.Contains
|
|
|
|
if strings.Contains(err.Error(), "UUID") {
|
|
|
|
return nil, BadRequestError{Reason: err.Error()}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// This shouldn't happen since the called API documents it shouldn't,
|
|
|
|
// but we check since the alternative if it happens is a panic.
|
|
|
|
if len(reply.Intentions) == 0 {
|
|
|
|
resp.WriteHeader(http.StatusNotFound)
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return reply.Intentions[0], nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// IntentionSpecific handles the endpoint for /v1/connect/intentions/:id
|
2020-09-04 18:42:15 +00:00
|
|
|
func (s *HTTPHandlers) IntentionSpecific(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2018-02-28 23:54:48 +00:00
|
|
|
id := strings.TrimPrefix(req.URL.Path, "/v1/connect/intentions/")
|
|
|
|
|
|
|
|
switch req.Method {
|
|
|
|
case "GET":
|
|
|
|
return s.IntentionSpecificGet(id, resp, req)
|
|
|
|
|
|
|
|
case "PUT":
|
2018-03-02 00:53:31 +00:00
|
|
|
return s.IntentionSpecificUpdate(id, resp, req)
|
2018-02-28 23:54:48 +00:00
|
|
|
|
|
|
|
case "DELETE":
|
2018-03-01 23:54:03 +00:00
|
|
|
return s.IntentionSpecificDelete(id, resp, req)
|
2018-02-28 23:54:48 +00:00
|
|
|
|
|
|
|
default:
|
|
|
|
return nil, MethodNotAllowedError{req.Method, []string{"GET", "PUT", "DELETE"}}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// GET /v1/connect/intentions/:id
|
2020-09-04 18:42:15 +00:00
|
|
|
func (s *HTTPHandlers) IntentionSpecificGet(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2018-02-28 23:54:48 +00:00
|
|
|
// Method is tested in IntentionEndpoint
|
|
|
|
|
|
|
|
args := structs.IntentionQueryRequest{
|
|
|
|
IntentionID: id,
|
|
|
|
}
|
|
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var reply structs.IndexedIntentions
|
|
|
|
if err := s.agent.RPC("Intention.Get", &args, &reply); err != nil {
|
|
|
|
// We have to check the string since the RPC sheds the error type
|
|
|
|
if err.Error() == consul.ErrIntentionNotFound.Error() {
|
|
|
|
resp.WriteHeader(http.StatusNotFound)
|
|
|
|
fmt.Fprint(resp, err.Error())
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2018-06-27 05:40:06 +00:00
|
|
|
// Not ideal, but there are a number of error scenarios that are not
|
|
|
|
// user error (400). We look for a specific case of invalid UUID
|
|
|
|
// to detect a parameter error and return a 400 response. The error
|
|
|
|
// is not a constant type or message, so we have to use strings.Contains
|
|
|
|
if strings.Contains(err.Error(), "UUID") {
|
|
|
|
return nil, BadRequestError{Reason: err.Error()}
|
|
|
|
}
|
|
|
|
|
2018-02-28 23:54:48 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-03-03 18:12:05 +00:00
|
|
|
// This shouldn't happen since the called API documents it shouldn't,
|
|
|
|
// but we check since the alternative if it happens is a panic.
|
|
|
|
if len(reply.Intentions) == 0 {
|
|
|
|
resp.WriteHeader(http.StatusNotFound)
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2018-02-28 23:54:48 +00:00
|
|
|
return reply.Intentions[0], nil
|
|
|
|
}
|
|
|
|
|
2018-03-02 00:53:31 +00:00
|
|
|
// PUT /v1/connect/intentions/:id
|
2020-09-04 18:42:15 +00:00
|
|
|
func (s *HTTPHandlers) IntentionSpecificUpdate(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2018-03-02 00:53:31 +00:00
|
|
|
// Method is tested in IntentionEndpoint
|
|
|
|
|
2020-06-26 21:59:15 +00:00
|
|
|
var entMeta structs.EnterpriseMeta
|
|
|
|
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-03-02 00:53:31 +00:00
|
|
|
args := structs.IntentionRequest{
|
|
|
|
Op: structs.IntentionOpUpdate,
|
|
|
|
}
|
|
|
|
s.parseDC(req, &args.Datacenter)
|
|
|
|
s.parseToken(req, &args.Token)
|
2019-10-29 18:13:36 +00:00
|
|
|
if err := decodeBody(req.Body, &args.Intention); err != nil {
|
2019-06-18 00:52:01 +00:00
|
|
|
return nil, BadRequestError{Reason: fmt.Sprintf("Request decode failed: %v", err)}
|
2018-03-02 00:53:31 +00:00
|
|
|
}
|
|
|
|
|
2020-06-26 21:59:15 +00:00
|
|
|
args.Intention.FillNonDefaultNamespaces(&entMeta)
|
|
|
|
|
2018-03-02 00:53:31 +00:00
|
|
|
// Use the ID from the URL
|
|
|
|
args.Intention.ID = id
|
|
|
|
|
|
|
|
var reply string
|
|
|
|
if err := s.agent.RPC("Intention.Apply", &args, &reply); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-06-13 10:54:27 +00:00
|
|
|
// Update uses the same create response
|
|
|
|
return intentionCreateResponse{reply}, nil
|
2018-03-02 00:53:31 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2018-03-01 23:54:03 +00:00
|
|
|
// DELETE /v1/connect/intentions/:id
|
2020-09-04 18:42:15 +00:00
|
|
|
func (s *HTTPHandlers) IntentionSpecificDelete(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2018-03-01 23:54:03 +00:00
|
|
|
// Method is tested in IntentionEndpoint
|
|
|
|
|
|
|
|
args := structs.IntentionRequest{
|
|
|
|
Op: structs.IntentionOpDelete,
|
|
|
|
Intention: &structs.Intention{ID: id},
|
|
|
|
}
|
|
|
|
s.parseDC(req, &args.Datacenter)
|
|
|
|
s.parseToken(req, &args.Token)
|
|
|
|
|
|
|
|
var reply string
|
|
|
|
if err := s.agent.RPC("Intention.Apply", &args, &reply); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-06-13 10:54:27 +00:00
|
|
|
return true, nil
|
2018-03-01 23:54:03 +00:00
|
|
|
}
|
|
|
|
|
2018-02-28 22:02:00 +00:00
|
|
|
// intentionCreateResponse is the response structure for creating an intention.
|
|
|
|
type intentionCreateResponse struct{ ID string }
|
2018-03-02 22:17:21 +00:00
|
|
|
|
|
|
|
// parseIntentionMatchEntry parses the query parameter for an intention
|
|
|
|
// match query entry.
|
2020-06-26 21:59:15 +00:00
|
|
|
func parseIntentionMatchEntry(input string, entMeta *structs.EnterpriseMeta) (structs.IntentionMatchEntry, error) {
|
2018-03-02 22:17:21 +00:00
|
|
|
var result structs.IntentionMatchEntry
|
2020-06-26 21:59:15 +00:00
|
|
|
result.Namespace = entMeta.NamespaceOrEmpty()
|
2018-03-02 22:17:21 +00:00
|
|
|
|
|
|
|
// Get the index to the '/'. If it doesn't exist, we have just a name
|
|
|
|
// so just set that and return.
|
|
|
|
idx := strings.IndexByte(input, '/')
|
|
|
|
if idx == -1 {
|
|
|
|
result.Name = input
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
result.Namespace = input[:idx]
|
|
|
|
result.Name = input[idx+1:]
|
|
|
|
if strings.IndexByte(result.Name, '/') != -1 {
|
|
|
|
return result, fmt.Errorf("input can contain at most one '/'")
|
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|