-
Notifications
You must be signed in to change notification settings - Fork 544
Add a new name resolution method: structuredformat #4085
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
seal90
wants to merge
8
commits into
dapr:main
Choose a base branch
from
seal90:config-string
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+660
−0
Open
Changes from 6 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
aa23b2d
Add a new name resolution method: structuredformat.
seal90 40d44f8
Merge branch 'dapr:main' into config-string
seal90 1b7b343
fix checkstyle error
seal90 4c7daec
Merge branch 'dapr:main' into config-string
seal90 b268a9b
Merge branch 'config-string' of https://github.com/seal90/components-…
seal90 550b60f
Merge branch 'main' into config-string
sicoyle a5fd97b
optimize structuredformat code and docs
seal90 9ce8641
Merge branch 'config-string' of https://github.com/seal90/components-…
seal90 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| # Structured Format Name Resolution | ||
|
|
||
| The Structured Format name resolver provides a flexible way to define services and their instances in a structured format via JSON or YAML configuration files, suitable for scenarios where explicit declaration of service topology is required. | ||
|
|
||
| ## Configuration Format | ||
|
|
||
| To use the Structured Format name resolver, create a configuration in your Dapr environment: | ||
|
|
||
| ```yaml | ||
| apiVersion: dapr.io/v1alpha1 | ||
| kind: Configuration | ||
| metadata: | ||
| name: appconfig | ||
| spec: | ||
| nameResolution: | ||
| component: "structuredformat" | ||
| configuration: | ||
| structuredType: "jsonString" | ||
| stringValue: '{"appInstances":{"myapp":[{"domain":"","ipv4":"127.0.0.1","ipv6":"","port":4433,"extendedInfo":{"hello":"world"}}]}}' | ||
| ``` | ||
|
|
||
| ## Configuration Fields | ||
|
|
||
| | Field | Required | Details | Example | | ||
| |---------|----------|---------|---------| | ||
| | structuredType | Y | Structured type: jsonString, yamlString, jsonFile, yamlFile. | jsonString | | ||
| | stringValue | N | This field must be configured when structuredType is set to jsonString or yamlString. | {"appInstances":{"myapp":[{"domain":"","ipv4":"127.0.0.1","ipv6":"","port":4433,"extendedInfo":{"hello":"world"}}]}} | | ||
| | filePath | N | This field must be configured when structuredType is set to jsonFile or yamlFile. | /path/to/yamlfile.yaml | | ||
|
|
||
|
|
||
| ## Examples | ||
|
|
||
| ```yaml | ||
| apiVersion: dapr.io/v1alpha1 | ||
| kind: Configuration | ||
| metadata: | ||
| name: appconfig | ||
| spec: | ||
| nameResolution: | ||
| component: "structuredformat" | ||
| configuration: | ||
| structuredType: "jsonString" | ||
| stringValue: '{"appInstances":{"myapp":[{"domain":"","ipv4":"127.0.0.1","ipv6":"","port":4433,"extendedInfo":{"hello":"world"}}]}}' | ||
| ``` | ||
|
|
||
| ```yaml | ||
| apiVersion: dapr.io/v1alpha1 | ||
| kind: Configuration | ||
| metadata: | ||
| name: appconfig | ||
| spec: | ||
| nameResolution: | ||
| component: "structuredformat" | ||
| configuration: | ||
| structuredType: "yamlString" | ||
| stringValue: | | ||
| appInstances: | ||
| myapp: | ||
| - domain: "" | ||
| ipv4: "127.0.0.1" | ||
| ipv6: "" | ||
| port: 4433 | ||
| extendedInfo: | ||
| hello: world | ||
| ``` | ||
|
|
||
| - Service ID "myapp" → "127.0.0.1:4433" | ||
|
|
||
|
|
||
| ## Notes | ||
|
|
||
| - Empty service IDs are not allowed and will result in an error | ||
| - Accessing a non-existent service will also result in an error | ||
| - The structured format string must be provided in the configuration | ||
| - The program selects the first available address according to the priority order: domain → IPv4 → IPv6, and appends the port to form the final target address | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| /* | ||
| Copyright 2025 The Dapr Authors | ||
| 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 structuredformat | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "math/rand" | ||
| "net" | ||
| "os" | ||
| "reflect" | ||
| "strconv" | ||
| "strings" | ||
|
|
||
| yaml "gopkg.in/yaml.v3" | ||
|
|
||
| "github.com/dapr/components-contrib/metadata" | ||
| nr "github.com/dapr/components-contrib/nameresolution" | ||
| "github.com/dapr/kit/logger" | ||
| kitmd "github.com/dapr/kit/metadata" | ||
| ) | ||
|
|
||
| const ( | ||
| JSONStringStructuredValue = "jsonString" | ||
| YAMLStringStructuredValue = "yamlString" | ||
| JSONFileStructuredValue = "jsonFile" | ||
| YAMLFileStructuredValue = "yamlFile" | ||
| ) | ||
|
|
||
| var allowedStructuredTypes = []string{ | ||
| JSONStringStructuredValue, | ||
| YAMLStringStructuredValue, JSONFileStructuredValue, YAMLFileStructuredValue, | ||
| } | ||
|
|
||
| // StructuredFormatResolver parses service names from a structured string | ||
| // defined in the configuration. | ||
| type StructuredFormatResolver struct { | ||
| meta structuredFormatMetadata | ||
| instances appInstances | ||
|
|
||
| logger logger.Logger | ||
| } | ||
|
|
||
| // structuredFormatMetadata represents the structured string (such as JSON or YAML) | ||
| // provided in the configuration for name resolution. | ||
| type structuredFormatMetadata struct { | ||
| StructuredType string | ||
| StringValue string | ||
| FilePath string | ||
| } | ||
|
|
||
| // appInstances stores the relationship between services and their instances. | ||
| type appInstances struct { | ||
| AppInstances map[string][]address `json:"appInstances" yaml:"appInstances"` | ||
| } | ||
|
|
||
| // address contains service instance information, including Domain, IPv4, IPv6, Port, | ||
| // | ||
| // and ExtendedInfo. | ||
| type address struct { | ||
| Domain string `json:"domain" yaml:"domain"` | ||
| IPV4 string `json:"ipv4" yaml:"ipv4"` | ||
| IPV6 string `json:"ipv6" yaml:"ipv6"` | ||
| Port int `json:"port" yaml:"port"` | ||
| ExtendedInfo map[string]string `json:"extendedInfo" yaml:"extendedInfo"` | ||
seal90 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // NewResolver creates a new Structured Format resolver. | ||
| func NewResolver(logger logger.Logger) nr.Resolver { | ||
| return &StructuredFormatResolver{ | ||
| logger: logger, | ||
| } | ||
| } | ||
|
|
||
| // Init initializes the structured format resolver with the given metadata. | ||
| func (r *StructuredFormatResolver) Init(ctx context.Context, metadata nr.Metadata) error { | ||
| var meta structuredFormatMetadata | ||
| err := kitmd.DecodeMetadata(metadata.Configuration, &meta) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to decode metadata: %w", err) | ||
| } | ||
|
|
||
| switch meta.StructuredType { | ||
| case JSONStringStructuredValue, YAMLStringStructuredValue: | ||
| if meta.StringValue == "" { | ||
| return fmt.Errorf("structuredType = %s, stringValue must be not empty", meta.StructuredType) | ||
| } | ||
| case JSONFileStructuredValue, YAMLFileStructuredValue: | ||
| if meta.FilePath == "" { | ||
| return fmt.Errorf("structuredType = %s, filePath must be not empty", meta.StructuredType) | ||
| } | ||
| default: | ||
| return fmt.Errorf("structuredType must be one of: %s", | ||
| strings.Join(allowedStructuredTypes, ", ")) | ||
| } | ||
|
|
||
| r.meta = meta | ||
|
|
||
| instances, err := loadStructuredFormatData(r) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| r.instances = instances | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // ResolveID resolves a service ID to an address using the configured value. | ||
| func (r *StructuredFormatResolver) ResolveID(ctx context.Context, req nr.ResolveRequest) (string, error) { | ||
| if req.ID == "" { | ||
| return "", errors.New("empty ID not allowed") | ||
| } | ||
|
|
||
| if addresses, exists := r.instances.AppInstances[req.ID]; exists && len(addresses) > 0 { | ||
| // gosec is complaining that we are using a non-crypto-safe PRNG. This is fine in this scenario since we are using it only for selecting a random address for load-balancing. | ||
| //nolint:gosec | ||
| address := addresses[rand.Int()%len(addresses)] | ||
|
|
||
| net.JoinHostPort(address.Domain, strconv.Itoa(address.Port)) | ||
| if address.Domain != "" { | ||
| return net.JoinHostPort(address.Domain, strconv.Itoa(address.Port)), nil | ||
| } else if address.IPV4 != "" { | ||
| return net.JoinHostPort(address.IPV4, strconv.Itoa(address.Port)), nil | ||
| } else if address.IPV6 != "" { | ||
| return net.JoinHostPort(address.IPV6, strconv.Itoa(address.Port)), nil | ||
| } | ||
| } | ||
|
|
||
| return "", fmt.Errorf("no services found with AppID '%s'", req.ID) | ||
| } | ||
|
|
||
| // Close implements io.Closer | ||
| func (r *StructuredFormatResolver) Close() error { | ||
| return nil | ||
| } | ||
|
|
||
| // GetComponentMetadata returns the metadata information for the component. | ||
| func (r *StructuredFormatResolver) GetComponentMetadata() metadata.MetadataMap { | ||
| metadataInfo := metadata.MetadataMap{} | ||
| metadata.GetMetadataInfoFromStructType(reflect.TypeOf(structuredFormatMetadata{}), | ||
| &metadataInfo, metadata.NameResolutionType) | ||
| return metadataInfo | ||
| } | ||
|
|
||
| // loadStructuredFormatData loads the mapping between services and their instances from a configuration file. | ||
| func loadStructuredFormatData(r *StructuredFormatResolver) (appInstances, error) { | ||
| var instances appInstances | ||
| switch r.meta.StructuredType { | ||
| case JSONStringStructuredValue: | ||
| err := json.Unmarshal([]byte(r.meta.StringValue), &instances) | ||
| if err != nil { | ||
| return instances, err | ||
| } | ||
| case YAMLStringStructuredValue: | ||
| err := yaml.Unmarshal([]byte(r.meta.StringValue), &instances) | ||
| if err != nil { | ||
| return instances, err | ||
| } | ||
| case JSONFileStructuredValue: | ||
| data, err := os.ReadFile(r.meta.FilePath) | ||
| if err != nil { | ||
| return instances, fmt.Errorf("error reading file: %s", err) | ||
| } | ||
|
|
||
| err = json.Unmarshal(data, &instances) | ||
| if err != nil { | ||
| return instances, err | ||
| } | ||
| case YAMLFileStructuredValue: | ||
| data, err := os.ReadFile(r.meta.FilePath) | ||
| if err != nil { | ||
| return instances, fmt.Errorf("error reading file: %s", err) | ||
| } | ||
|
|
||
| err = yaml.Unmarshal(data, &instances) | ||
| if err != nil { | ||
| return instances, err | ||
| } | ||
| default: | ||
| return instances, fmt.Errorf("structuredType must be one of: %s", | ||
| strings.Join(allowedStructuredTypes, ", ")) | ||
| } | ||
| return instances, nil | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.