Skip to content
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

Render platform overrides in the UI #8026

Merged
merged 2 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions app/invocation/invocation.css
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,14 @@
font-size: 14px;
}

.action-section .platform-property-overridden * {
color: #757575;
}

.action-section .platform-property-note {
color: #757575;
}

.action-section .grpc-status-error {
color: #d32f2f;
}
Expand Down Expand Up @@ -1057,10 +1065,6 @@
font-weight: 600;
}

.prop-value {
color: #444;
}

.prop-value .detail {
color: #aaa;
}
Expand Down
61 changes: 46 additions & 15 deletions app/invocation/invocation_action_card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { BuildBuddyError, HTTPStatusError } from "../util/errors";
import { Profile, readProfile } from "../trace/trace_events";
import TraceViewer from "../trace/trace_viewer";
import Spinner from "../components/spinner/spinner";
import { timestampToDate } from "../util/proto";
import { MessageClass, timestampToDate } from "../util/proto";

type Timestamp = google_timestamp.protobuf.Timestamp;
type ITimestamp = google_timestamp.protobuf.ITimestamp;
Expand Down Expand Up @@ -569,23 +569,21 @@ export default class InvocationActionCardComponent extends React.Component<Props
.catch((e) => console.error(e));
}

// For firecracker actions, VM metadata is stored in the auxiliary metadata field
// of the execution metadata. Try to decode it into an object if it exists.
private getFirecrackerVMMetadata(): firecracker.VMMetadata | null | undefined {
const auxiliaryMetadata = this.state.actionResult?.executionMetadata?.auxiliaryMetadata;
if (!auxiliaryMetadata || auxiliaryMetadata.length == 0) {
return null;
}
for (const metadata of auxiliaryMetadata) {
if (metadata.typeUrl === "type.googleapis.com/firecracker.VMMetadata") {
return firecracker.VMMetadata.decode(metadata.value);
/**
* Looks for the given message type in auxiliary metadata and returns the
* decoded message if found.
*/
private getAuxiliaryMetadata<T>(messageClass: MessageClass<T>): T | null | undefined {
for (const metadata of this.state.actionResult?.executionMetadata?.auxiliaryMetadata ?? []) {
if (metadata.typeUrl === messageClass.getTypeUrl()) {
return messageClass.decode(metadata.value);
}
}
return null;
}

private getVMPreviousTaskHref(): string {
const vmMetadata = this.getFirecrackerVMMetadata();
const vmMetadata = this.getAuxiliaryMetadata(firecracker.VMMetadata);
const task = vmMetadata?.lastExecutedTask;
if (!task?.executeResponseDigest || !task?.invocationId || !task?.actionDigest) return "";
return `/invocation/${task.invocationId}?actionDigest=${digestToString(
Expand Down Expand Up @@ -880,11 +878,28 @@ export default class InvocationActionCardComponent extends React.Component<Props
);
}

private getPlatformOverrides(): Map<string, string> {
const overrides = new Map<string, string>();
const executionAuxiliaryMetadata = this.getAuxiliaryMetadata(execution_stats.ExecutionAuxiliaryMetadata);
for (const prop of executionAuxiliaryMetadata?.platformOverrides?.properties ?? []) {
let value = prop.value ?? "";
// TODO: this redaction is also done on the server and can be removed
// after some time.
const nameLower = prop.name.toLowerCase();
if (nameLower.includes("username") || nameLower.includes("password") || nameLower.includes("env-overrides")) {
value = "<REDACTED>";
}
overrides.set(prop.name, value);
}
return overrides;
}

render() {
const digest = parseActionDigest(this.props.search.get("actionDigest") ?? "");
if (!digest) return <></>;
const vmMetadata = this.getFirecrackerVMMetadata();
const vmMetadata = this.getAuxiliaryMetadata(firecracker.VMMetadata);
const executionId = this.getExecutionId();
const platformOverrides = this.getPlatformOverrides();

return (
<div className="invocation-action-card">
Expand Down Expand Up @@ -1009,17 +1024,33 @@ export default class InvocationActionCardComponent extends React.Component<Props
{this.state.command.platform?.properties.length ? (
<div className="action-list">
{this.state.command.platform?.properties.map((property) => (
<div>
<div
className={
platformOverrides.has(property?.name ?? "") ? "platform-property-overridden" : ""
}>
<span className="prop-name">{property.name}</span>
<span className="prop-value">={property.value}</span>
{platformOverrides.has(property?.name ?? "") && <span> (overridden)</span>}
</div>
))}
{!this.state.command.platform?.properties.length && <div>(Default)</div>}
</div>
) : (
<div>None</div>
)}
</div>
{platformOverrides.size > 0 && (
<div className="action-section">
<div className="action-property-title">Platform overrides</div>
<div className="action-list">
{[...platformOverrides.entries()].map(([name, value]) => (
<div>
<span className="prop-name">{name}</span>
<span className="prop-value">={value}</span>
</div>
))}
</div>
</div>
)}
{!this.state.actionResult && this.renderExpectedOutputs(this.state.command)}
</div>
) : (
Expand Down
1 change: 1 addition & 0 deletions app/util/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ ts_library(
"//proto:timestamp_ts_proto",
"@npm//@types/long",
"@npm//long",
"@npm//protobufjs",
],
)

Expand Down
20 changes: 20 additions & 0 deletions app/util/proto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,27 @@
import Long from "long";
import { Reader } from "protobufjs";
import { google as google_timestamp } from "../../proto/timestamp_ts_proto";
import { google as google_duration } from "../../proto/duration_ts_proto";

/**
* Generic interface exposed by message classes.
*
* This exposes the static methods from the generated message classes. Note:
* class objects are not the same as _instances_ of the class:
*
* ```
* // Create an instance of `message FooMsg`:
* const fooInstance = new FooMsg({ someField: 'someValue' });
* // Note the types of `fooInstance` and `FooMsg`:
* let value1: FooMsg = fooInstance;
* let value2: MessageClass<FooMsg> = FooMsg;
* ```
*/
export type MessageClass<T> = {
decode(source: Reader | Uint8Array, length?: number): T;
getTypeUrl(): string;
};

export function dateToTimestamp(date: Date): google_timestamp.protobuf.Timestamp {
const timestampMillis = date.getTime();
return new google_timestamp.protobuf.Timestamp({
Expand Down
1 change: 1 addition & 0 deletions enterprise/server/remote_execution/execution_server/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ go_library(
"@org_golang_google_genproto//googleapis/longrunning",
"@org_golang_google_grpc//metadata",
"@org_golang_google_grpc//status",
"@org_golang_google_protobuf//types/known/anypb",
"@org_golang_google_protobuf//types/known/timestamppb",
"@org_golang_x_time//rate",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
"golang.org/x/time/rate"
"google.golang.org/genproto/googleapis/longrunning"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/timestamppb"

espb "github.com/buildbuddy-io/buildbuddy/proto/execution_stats"
Expand Down Expand Up @@ -1138,6 +1139,7 @@ func (s *ExecutionServer) cacheExecuteResponse(ctx context.Context, taskID strin
}
arn := digest.NewResourceName(d, taskRN.GetInstanceName(), rspb.CacheType_AC, taskRN.GetDigestFunction())

redactCachedExecuteResponse(ctx, response)
b, err := proto.Marshal(response)
if err != nil {
return err
Expand Down Expand Up @@ -1239,6 +1241,36 @@ func (s *ExecutionServer) fetchActionAndCommand(ctx context.Context, actionResou
return action, cmd, nil
}

func redactCachedExecuteResponse(ctx context.Context, rsp *repb.ExecuteResponse) {
md := rsp.GetResult().GetExecutionMetadata()
for _, auxAny := range md.GetAuxiliaryMetadata() {
if auxAny.MessageIs(&espb.ExecutionAuxiliaryMetadata{}) {
redactExecutionAuxiliaryMetadata(ctx, auxAny)
}
}
}

func redactExecutionAuxiliaryMetadata(ctx context.Context, auxAny *anypb.Any) {
md := &espb.ExecutionAuxiliaryMetadata{}
if err := auxAny.UnmarshalTo(md); err != nil {
log.CtxErrorf(ctx, "Failed to unmarshal ExecutionAuxiliaryMetadata: %s", err)
return
}

// Redact platform overrides.
overrides := md.GetPlatformOverrides().GetProperties()
for _, p := range overrides {
name := strings.ToLower(p.GetName())
if strings.Contains(name, "password") || strings.Contains(name, "username") || strings.Contains(name, "env-overrides") {
p.Value = "<REDACTED>"
}
}

if err := auxAny.MarshalFrom(md); err != nil {
log.CtxErrorf(ctx, "Failed to marshal ExecutionAuxiliaryMetadata: %s", err)
}
}

func executionDuration(md *repb.ExecutedActionMetadata) (time.Duration, error) {
if err := md.GetWorkerStartTimestamp().CheckValid(); err != nil {
return 0, err
Expand Down
Loading