Skip to content

Conversation

@leggetter
Copy link
Collaborator

@leggetter leggetter commented May 26, 2025

See contributing/.plans/2025-05-26-config-documentation-synchronization.md

@vercel
Copy link

vercel bot commented May 26, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
outpost-docs ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 26, 2025 11:30am
outpost-website ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 26, 2025 11:30am

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR enhances the auto-generation of configuration documentation by augmenting configuration struct fields with additional struct tags (desc and required) to keep docs in sync with code. Key changes include:

  • Adding descriptive tags and required-status indicators across multiple configuration files.
  • Updating the go:generate directive and associated documentation plan to support automated documentation generation.
  • Expanding configuration details for various providers (AWS SQS, GCP Pub/Sub, RabbitMQ, Portal, OpenTelemetry, etc.).

Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.

Show a summary per file
File Description
internal/config/publishmq.go Added desc and required tags for PublishAWSSQSConfig, PublishGCPPubSubConfig, and PublishRabbitMQConfig.
internal/config/portal.go Added descriptive metadata to PortalConfig fields.
internal/config/otel.go Updated OpenTelemetry configuration structs with description and required-status tags.
internal/config/mq.go Augmented MQ provider configuration structs (AWSSQSConfig, GCPPubSubConfig, RabbitMQConfig, MQsConfig) with tags.
internal/config/destinations.go Enhanced destination configuration structs with descriptive tags.
internal/config/config.go Updated the main Config struct and related configurations with additional tags and a go:generate directive.
contributing/.plans/2025-05-26-config-documentation-synchronization.md Added a detailed plan outlining the approach for synchronizing configuration code and docs.

- Updated the required status of several configuration fields in the Config struct to improve clarity and flexibility.
- Adjusted descriptions for various fields to provide clearer guidance on their usage and requirements.
- Removed default values from descriptions where applicable to avoid confusion.
- Ensured consistency in required fields across different configuration types, including Redis, MQ, and Portal configurations.
- Enhanced webhook and AWS Kinesis configurations by refining descriptions and removing unnecessary defaults.
Comment on lines +157 to +160
SignatureContentTemplate: "{{.Timestamp.Unix}}.{{.Body}}",
SignatureHeaderTemplate: "t={{.Timestamp.Unix}},v0={{.Signatures | join \",\"}}",
SignatureEncoding: "hex",
SignatureAlgorithm: "hmac-sha256",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this is a logic change that has crept in. Need to check whether we should remove this. cc: @alexluong

Copy link
Collaborator

@alexluong alexluong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @leggetter, thanks for taking the initiative to set this up. The codegen command was interesting and seems to work quite well. Overall, the PR looks great and doesn't seem to affect the functionality. All test passing right now. We can merge this as is. I do have 2 notes:

  1. The docgen command logic is quite complicated (AST, ahhh). I didn't do a full audit but I did run it locally and seems like it works well. My only feedback is I think we could consider a different approach that is a bit simpler.

My understanding is we're parsing files within the internal/config package and using AST to detect config structs and parse through it.

I wonder if it's easier to use the reflect package to traverse the main Config struct. I'll share a snippet below, maybe it can be helpful if you want to explore this approach. Happy to just merge this for now and reiterate later.

  1. One semi-unrelated issue is the webhook header. I'll take a look later but if you have time, I'd appreciate it if you can help verify too. I think I changed the behavior of the signature where the default header "X-Outpost-Timestamp" is in epoch timestamp in millisecond, but in the "X-Outpost-Signature", the timestamp there is in second. Because of that, if you implement verification, it won't match correctly unless you convert ms into second first.

@alexluong
Copy link
Collaborator

func parseWithReflection() error {
	cfg := config.Config{} // Create an instance of the actual Config struct
	cfg.InitDefaults()     // Initialize it with default values

	// Start the recursive traversal
	fmt.Println("\n--- Starting Reflection-Based Parsing ---")
	parseTypeRecursive(reflect.TypeOf(cfg), reflect.ValueOf(cfg), "", 0)
	fmt.Println("--- Finished Reflection-Based Parsing ---")

	return nil
}

func parseTypeRecursive(typ reflect.Type, val reflect.Value, pathPrefix string, indentLevel int) {
	// If 'val' is a pointer, dereference it to get the actual struct value.
	// Also, update 'typ' to be the type of the dereferenced value.
	if val.Kind() == reflect.Ptr {
		if val.IsNil() { // Important: can't proceed if the pointer is nil
			indent := strings.Repeat("  ", indentLevel)
			fmt.Printf("%sStruct: %s (Path: %s) is a <nil pointer>\n", indent, typ.String(), pathPrefix)
			return
		}
		val = val.Elem()
		typ = val.Type() // Update typ to the element's type
	}

	// Ensure we are dealing with a struct.
	if val.Kind() != reflect.Struct {
		// This can happen if a field is a basic type, slice, map, etc.,
		// and a recursive call was made on it.
		// Or if the initial call wasn't a struct or pointer to struct.
		return
	}
	// typ should now definitely be a struct type because val is a struct value.

	indent := strings.Repeat("  ", indentLevel)
	fmt.Printf("%sStruct: %s (Path: %s)\n", indent, typ.Name(), pathPrefix) // typ.Name() is fine here

	configPkgPath := reflect.TypeOf(config.Config{}).PkgPath() // For checking recursion scope

	for i := 0; i < typ.NumField(); i++ {
		fieldDesc := typ.Field(i) // reflect.StructField - describes the field
		fieldVal := val.Field(i)  // reflect.Value - holds the runtime value of this field

		currentPath := fieldDesc.Name
		if pathPrefix != "" {
			currentPath = pathPrefix + "." + fieldDesc.Name
		}

		fmt.Printf("%s  Field: %s (Type: %s, Path: %s)\n", indent, fieldDesc.Name, fieldDesc.Type.String(), currentPath)
		fmt.Printf("%s    YAML: '%s', Env: '%s', Desc: '%s', Required: '%s'\n",
			indent,
			fieldDesc.Tag.Get("yaml"),
			fieldDesc.Tag.Get("env"),
			fieldDesc.Tag.Get("desc"),
			fieldDesc.Tag.Get("required"))

		// Correctly use fieldVal to get the current value
		var currentValueInterface interface{}
		if fieldVal.CanInterface() { // Check if the field is exported
			currentValueInterface = fieldVal.Interface()
			if fieldVal.Kind() == reflect.Ptr && fieldVal.IsNil() {
				currentValueInterface = "<nil pointer>"
			}
		} else {
			currentValueInterface = "<unexported field>"
		}
		fmt.Printf("%s    Current Value: %v\n", indent, currentValueInterface)

		// --- Recursive Call Logic ---
		// We need the type of the field for recursion checks, and the value for the next call.
		fieldTypeForRecursion := fieldDesc.Type
		fieldValueForRecursion := fieldVal

		// Determine the actual element type if fieldTypeForRecursion is a pointer
		actualFieldTypeForCheck := fieldTypeForRecursion
		if actualFieldTypeForCheck.Kind() == reflect.Ptr {
			actualFieldTypeForCheck = actualFieldTypeForCheck.Elem()
		}

		if actualFieldTypeForCheck.Kind() == reflect.Struct {
			if actualFieldTypeForCheck.PkgPath() == configPkgPath {
				// Pass the field's type and the field's value to the recursive call.
				parseTypeRecursive(fieldTypeForRecursion, fieldValueForRecursion, currentPath, indentLevel+1)
			} else {
				fmt.Printf("%s    (Skipping recursion into struct from different package: %s from %s)\n", indent, actualFieldTypeForCheck.Name(), actualFieldTypeForCheck.PkgPath())
			}
		}
	}
}

@leggetter
Copy link
Collaborator Author

leggetter commented May 29, 2025

One semi-unrelated issue is the webhook header. I'll take a look later but if you have time, I'd appreciate it if you can help verify too. I think I changed the behavior of the signature where the default header "X-Outpost-Timestamp" is in epoch timestamp in millisecond, but in the "X-Outpost-Signature", the timestamp there is in second. Because of that, if you implement verification, it won't match correctly unless you convert ms into second first.

Captured in #412

The docgen command logic is quite complicated (AST, ahhh). I didn't do a full audit but I did run it locally and seems like it works well. My only feedback is I think we could consider a different approach that is a bit simpler.
My understanding is we're parsing files within the internal/config package and using AST to detect config structs and parse through it.

I wonder if it's easier to use the reflect package to traverse the main Config struct. I'll share a snippet below, maybe it can be helpful if you want to explore this approach. Happy to just merge this for now and reiterate later.

Let's merge and I'll create an issue to investigate improving the docs generator #415

@leggetter leggetter merged commit 23433b8 into main May 29, 2025
4 checks passed
@leggetter leggetter deleted the feat/config-updates branch May 29, 2025 17:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants