-
Notifications
You must be signed in to change notification settings - Fork 0
Flesh out tests for types.go, parser.go; add validation_test.go, update validation.go; add canonical formatting for cpu and memory resources #1
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice changes! Generally I like the updates.
Only part to be discussed and considered is the consistency in the internal representation. I'm not a huge fan of a structure having two public fields (i.e. CPU
and CPURaw
) that are supposed to be consistent with each other. This can lead to possible inconsistency. We should consider a way to only have one field that'd act as the source of truth and render other views as methods.
} | ||
|
||
if err := normalizeCPU(&globalConfig.Resources); err != nil { | ||
return nil, fmt.Errorf("normalize cpu: %w", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make the error message more descriptive e.g. Failed to normalize CPU resource %v: %w
and printout the problematic resource string.
Memory: "8Gi", // Default Memory | ||
Storage: "20Gi", // Default Storage | ||
GPU: 0, // Default GPU | ||
CPU: 2000, // Default CPU (2 cores) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually think it'd be better for us to accept resource specification that's valid for Kubernetes. https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-units-in-kubernetes
|
||
// Memory returns the memory resource allocation as a string suitable for Kubernetes manifests. | ||
// Memory returns the canonical memory quantity formatted for Kubernetes. | ||
// Assumes Resources.Memory has been normalized to Mi (mebibytes). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why normalize to one specific unit?
} | ||
fmt.Printf(" Memory=%s", memStr) | ||
} | ||
fmt.Println() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit redundant now. Let's simplify.
fmt.Println() | |
hasCPU := cpuStr != "0" | |
hasMem := memStr != "" | |
if hasCPU || hasMem { | |
var parts []string | |
if hasCPU { | |
parts = append(parts, fmt.Sprintf("CPU=%s", cpuStr)) | |
} | |
if hasMem { | |
parts = append(parts, fmt.Sprintf("Memory=%s", memStr)) | |
} | |
fmt.Printf(" Resources: %s\n", strings.Join(parts, ", ")) | |
} |
sshKeys, err := cfg.GetSSHKeys() | ||
if err != nil { | ||
log.Fatal(err) | ||
fmt.Println("error:", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's keep error behavior consistent and have if log.Fatal
much like on line 16
config.DeveloperDir = developerDir | ||
|
||
if err := normalizeCPU(&config.Resources); err != nil { | ||
return nil, fmt.Errorf("normalize cpu: %w", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above: give more descriptive error message while showing the failed value.
GPU int `yaml:"gpu,omitempty" validate:"omitempty,min=0,max=8"` // Number of GPUs requested | ||
CPURaw any `yaml:"cpu,omitempty" validate:"omitempty,k8s_cpu"` // Can be string or int | ||
MemoryRaw any `yaml:"memory,omitempty" validate:"omitempty,k8s_memory"` | ||
CPU int64 `yaml:"-"` // canonical |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, I'd prefer that we call it CPU
and Memory
for the original and CanonicalCPU
and CanonicalMemory
to distinguish processed and canonicalized values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also having an entirely separate public field runs the risk of duplicated source of truth. Consequently, you should consider making the field private and accessed via methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than implementing CPU and Memory as an entirely separate and partly redundant fields, keep the original field of CPU and Memory as loaded from YAML but provide getCPU
and getMemory
that provides normalized numerical values of the CPU and Memory using your normalized[CPU|Memory]
functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've more carefully stepped through the code and suggested more concrete changes to be made. Speficially, I'd prefer we stick to original value of supplied memory and CPU to be stored in Memory and CPU field, but provide getMemory
and getCPU
methods that would provide fully normalized numerical values.
I've also implemented a few improvments in the code to be made -- please refer to the comments and let me know if you have any questions!
// User ID: 2000 | ||
// SSH Keys: 1 configured | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These functions are specifically added to serve as usage examples and thus should not be removed even if equivalent feature is tested in real testing. Please restore them
return nil, fmt.Errorf("normalize cpu: %w", err) | ||
} | ||
|
||
if err := normalizeMemory(&globalConfig.Resources); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The interface into normalizeCPU
and normalizeMemory
is a bit awkward, specifically working with Resources
. Either make them methods attached to the Resources
struct or turn them to simply accept a string that would return normalized values back.
// Step 6: Set developer directory and validate | ||
userConfig.DeveloperDir = developerDir | ||
|
||
// Step 7: Normalize flexible/raw fields → canonical representation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same set of comments as in the earlier usage of normalizeCPU
and normalizeMemory
// normalizeCPU reads r.CPURaw and sets r.CPU (millicores). | ||
// With strict policy, invalid or non-positive inputs propagate as zero with an error, or | ||
// you can choose to return ok=false and let the loader decide. Here I return an error. | ||
func normalizeCPU(r *ResourceConfig) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't modify the ResourceConfig. Rather accept the CPURaw
string value and simply return normalized numerical value.
|
||
// toMillicores converts a flexible CPU value to millicores. | ||
// Policy: syntactically valid but non-positive (<=0) values are INVALID → ok=false. | ||
func toMillicores(v any) (int64, bool) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this function is performing too many things at once. Consider splitting this into 1) a function to parse any
into a acceptable string and 2) something that takes the string input and provide normalized value readout numerically.
|
||
// toMi converts a flexible memory value into canonical Mi (mebibytes). | ||
// Strict policy: syntactically valid but non-positive (<= 0) values are INVALID (ok=false). | ||
func toMi(v any) (int64, bool) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extract utility functions like parseStringToMi
into a separate go file.
// Returns (0,false) if value <= 0 or on invalid input. | ||
func parseStringToMi(s string) (int64, bool) { | ||
// ---- Binary SI (powers of 1024) ---- | ||
if hasSuffixFold(s, "Ki") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reimplement this logic using map
from suffix to factor so that you don't repeat identical logic for every possible suffix.
GPU int `yaml:"gpu,omitempty" validate:"omitempty,min=0,max=8"` // Number of GPUs requested | ||
CPURaw any `yaml:"cpu,omitempty" validate:"omitempty,k8s_cpu"` // Can be string or int | ||
MemoryRaw any `yaml:"memory,omitempty" validate:"omitempty,k8s_memory"` | ||
CPU int64 `yaml:"-"` // canonical |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than implementing CPU and Memory as an entirely separate and partly redundant fields, keep the original field of CPU and Memory as loaded from YAML but provide getCPU
and getMemory
that provides normalized numerical values of the CPU and Memory using your normalized[CPU|Memory]
functions.
Storage: "20Gi", // Default Storage | ||
GPU: 0, // Default GPU | ||
CPU: 2000, // Default CPU (2 cores) | ||
Memory: 8 * 1024, // Default Memory (8Gi -> 8192Mi) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Revert back to using string values as the resource specification, and rather provide normalized numerical values as function outputs.
} | ||
} | ||
return keys, nil | ||
case int: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
make use of composite case like done in uint
cases
} | ||
|
||
// Small float guards for validator predicates (avoid NaN/Inf). | ||
func float32IsNaNOrInf(f float32) bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use math.IsNaN()
and math.IsInf()
instead to detect NaN and Inf
Breakdown of changes to each file:
parser.go
types.go
validation.go
parser_test.go
types_test.go
validation_test.go
renderer_test.go
statefulset.yaml
generate.go
example_test.go