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

frontend: Added displaying of imported environment variables per container. #2728

Open
wants to merge 17 commits into
base: main
Choose a base branch
from

Conversation

vlasov-y
Copy link

@vlasov-y vlasov-y commented Jan 3, 2025

Displaying of environment variables per container in pod is the main feature my developers used in kubernetes-dashboard. I have added it here as well.
Also, I have found an error in browser console and applied a small fix to TooltipLight file.

image

Features

  • 1. Values from secrets are hidden by default
  • 2. Secret values can be revealed on click
  • 3. If configmap or secret is younger than container start time (or pod creation) the warning is shown
  • 4. Variable name and value are copied to the clipboard on click
  • 5. Names of configmaps and secrets are valid links to the respective resources
  • 6. Handing of optional, resourceFieldRef and fieldRef variables is implemented

@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jan 3, 2025
@vlasov-y
Copy link
Author

vlasov-y commented Jan 3, 2025

Hi @illume @skoeva
Could you please take a look?

@gecube
Copy link

gecube commented Jan 3, 2025

Oh... in lens there is an incorrect behaviour. TLDR: you should SHOW NOT THE current ENVs from secret and / or configmap, but CURRENT IN THE POD (which is effectively impossible).

@vlasov-y
Copy link
Author

vlasov-y commented Jan 3, 2025

Oh... in lens there is an incorrect behaviour. TLDR: you should SHOW NOT THE current ENVs from secret and / or configmap, but CURRENT IN THE POD (which is effectively impossible).

Agree, that would be much better, but I could not find a way to do that.

@gecube
Copy link

gecube commented Jan 3, 2025

@vlasov-y it is mandatory, otherwise this feature will not work properly. My developer asked me about "why application is not working, the env variables are shown properly in Lens"... it took for me 1h to explain him, that lens shown not reality, but it's own hallucinations...

@vlasov-y
Copy link
Author

vlasov-y commented Jan 3, 2025

@vlasov-y it is mandatory, otherwise this feature will not work properly. My developer asked me about "why application is not working, the env variables are shown properly in Lens"... it took for me 1h to explain him, that lens shown not reality, but it's own hallucinations...

I had same situations. But it is better than nothing and as far as I know, env variables (if they are overriden) will differ per process in container, are not they? Some containers may not have a shell as well.
On the current project, developers use these values to get database credentials or URLs.

@gecube
Copy link

gecube commented Jan 3, 2025

@vlasov-y it could lead to improper expectations and even more debug than necessary.

But it is better than nothing and as far as I know, env variables (if they are overriden) will differ per process in container, are not they? Some containers may not have a shell as well.

yes, but we are talking about container envs that is equal to PID1 process envs.

Again. Situation is very simple. You run a pod. You show envs in dashboard. Then somebody changed configmap or secret. What would be shown - the new values from CM or secret or actual values from pod? I know the answer and it is not satisfactory.

@vlasov-y
Copy link
Author

vlasov-y commented Jan 3, 2025

@vlasov-y it could lead to improper expectations and even more debug than necessary.

But it is better than nothing and as far as I know, env variables (if they are overriden) will differ per process in container, are not they? Some containers may not have a shell as well.

yes, but we are talking about container envs that is equal to PID1 process envs.

Again. Situation is very simple. You run a pod. You show envs in dashboard. Then somebody changed configmap or secret. What would be shown - the new values from CM or secret or actual values from pod? I know the answer and it is not satisfactory.

New value will be shown of course, but reloader will save us from issues with that.
Anyway, I 100% agree with you, @gecube, I just have done what I could and I believe that is better than nothing.

@gecube
Copy link

gecube commented Jan 3, 2025

@vlasov-y you should not mention the stakater here as it is some design flaw not related to it.

Let's discuss all possible options

  • not to implement this feature at all. I think it is counter-productive as it would be useful for many users
  • so we need somehow to fix the inconsistency or stale data I explained. The first option could be to check pod start time and the data of configmap change and then show warning that data is inaccurate (or could be inaccurate!!!!)
  • or even we can just go directly to pod and print the PID1 env variables and match them back to UI (then the question arises could they be changed in some way after pod was running?) Or go directly to CRI (container engine) and ask it about envs that we injected during pod startup (but it would be, of course, CRI-specific and dependant on its' API)
  • and ANYWAY we should show warning if secret/configmap was removed and pod using envs from it is still running (so we can't retrieve the env values from k8s api)....

Any ideas / suggestions here?

@vlasov-y
Copy link
Author

vlasov-y commented Jan 3, 2025

@vlasov-y you should not mention the stakater here as it is some design flaw not related to it.

Let's discuss all possible options

  • not to implement this feature at all. I think it is counter-productive as it would be useful for many users
  • so we need somehow to fix the inconsistency or stale data I explained. The first option could be to check pod start time and the data of configmap change and then show warning that data is inaccurate (or could be inaccurate!!!!)
  • or even we can just go directly to pod and print the PID1 env variables and match them back to UI (then the question arises could they be changed in some way after pod was running?) Or go directly to CRI (container engine) and ask it about envs that we injected during pod startup (but it would be, of course, CRI-specific and dependant on its' API)
  • and ANYWAY we should show warning if secret/configmap was removed and pod using envs from it is still running (so we can't retrieve the env values from k8s api)....

Any ideas / suggestions here?

p.1 - agree
p.2 - that is definitely possible and I can do that, I suggest to start with that at least
p.3 - headlamp frontend uses custom library to interact with Kubernetes, I am not sure it can make pod execs. I assume you suggest to make pod exec and print PID 1 env from /proc/1/env or similar? What is pod won't have shell inside?
p.4 - I believe it is implemented already. Error message will be printed instead of the configmap/secret value in case of any access error, including not found.

@vlasov-y
Copy link
Author

vlasov-y commented Jan 3, 2025

FYI @gecube
image

That is what I have, but there is an issue. I compare configmap/secret creationTimestamp with Pod's creation timestamp. There is no updateTimestamp in configmap, while I can change its data without changing the creationTimestamp. Do you have any ideas?

@gecube
Copy link

gecube commented Jan 3, 2025

@vlasov-y no idea to be honest....

@gecube
Copy link

gecube commented Jan 3, 2025

p.3 - headlamp frontend uses custom library to interact with Kubernetes, I am not sure it can make pod execs. I assume you suggest to make pod exec and print PID 1 env from /proc/1/env or similar? What is pod won't have shell inside?

then we may somehow get data from CRI. The thesis about absence of shell is totally correct, but it happens only for a small subset of images.

@vlasov-y
Copy link
Author

vlasov-y commented Jan 3, 2025

p.3 - headlamp frontend uses custom library to interact with Kubernetes, I am not sure it can make pod execs. I assume you suggest to make pod exec and print PID 1 env from /proc/1/env or similar? What is pod won't have shell inside?

then we may somehow get data from CRI. The thesis about absence of shell is totally correct, but it happens only for a small subset of images.

UPD I have understood that comparing with metadata.creationTimestamp is not always correct because containers in the pod can be restarted on failure and internet says that they will consume newer version of configmap. Hence, I have used .status.containerStatuses[container by name].status.running.startedAt and only if it is not available I am falling back to creation timestamp.

Regarding a small subset of images: maybe 1/3 of images in my clusters do not have shell. Most kubernets Go operator does not have one, since they are launched in google's distroless image. So that will be an often case.

Honestly, I think that this disclaimer will be enough :)

@vlasov-y vlasov-y marked this pull request as draft January 3, 2025 16:02
@vlasov-y vlasov-y marked this pull request as ready for review January 3, 2025 17:31
@vlasov-y vlasov-y changed the title Added displaying of imported environment variables per container. frontend: Added displaying of imported environment variables per container. Jan 3, 2025
@dosubot dosubot bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Jan 3, 2025
@joaquimrocha
Copy link
Collaborator

I have read the thread (thanks for the feature and the discussion, @vlasov-y and @gecube ). I think I understand the issue (the UI may hint to an instance of a secret/config-map that's not the one being used by the pod and thus the values may differ).
IMO the hint is good enough as a way to mitigate the problem. And in the future we may add more ways if possible (like checking the shell if needed, but there's a performance impact we should evaluate AFAIU)
@gecube Are you fine moving forward with this solution? (we are yet to review the code though)

Copy link
Collaborator

@joaquimrocha joaquimrocha left a comment

Choose a reason for hiding this comment

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

Left a few comments but would like @sniok to take a look as well.
There's also a Merge branch that should be removed from the PR (by doing git rebase), and please check our git guidelines for the commit messages: https://headlamp.dev/docs/latest/contributing#2-follow-commit-guidelines

It's an awesome feature, @vlasov-y . Let's work together to land it.

@@ -1,6 +1,7 @@
import { Icon } from '@iconify/react';
import Editor from '@monaco-editor/react';
import { InputLabel } from '@mui/material';
import { Visibility, VisibilityOff } from '@mui/icons-material'; // Material icons for eye
Copy link
Collaborator

Choose a reason for hiding this comment

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

We use iconify instead of MUI icons in the UI. The equivalent icon is mdi:eye and mdi:eye-off, but I'd rather use the SecretField we have in this file.

Copy link
Author

Choose a reason for hiding this comment

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

Resolved in ed991a3
Please review and resolve this conversation if it is fine.

isSecret: boolean;
};

export function GetEnvironmentVariables(props: EnvironmentVariablesProps) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

You could call this ContainerEnvironmentVariables as it sounds more in line with the rest of the components.
Also, I am not sure we should export this for now. Maybe we keep it unexported for now.

Copy link
Author

Choose a reason for hiding this comment

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

Resolved in 3ac20cb
Please review and resolve this conversation if it is fine.

P.S. 'Clean code' says that function name must contain a verb, but I do not mind renaming the function as you have suggested.

Comment on lines 774 to 796
<Box display="flex" alignItems="center">
{data.isError && (
<Box aria-label="hidden" display="flex" alignItems="center" px={1}>
<Icon icon="mdi:alert-outline" width="1.3rem" height="1.3rem" aria-label="error" />
<Typography color="error" sx={{ marginLeft: 1 }}>
{displayValue}
</Typography>
</Box>
)}
{!data.isError && (
<Box onClick={() => handleCopy(data.value)}>
<StatusLabel status="" sx={{ fontFamily: 'monospace' }}>
{displayValue}
</StatusLabel>
<Snackbar open={copied} message="Copied!" autoHideDuration={2000} />
</Box>
)}
{!data.isError && data.isSecret && (
<IconButton onClick={() => toggleSecret(data.key)} sx={{ marginLeft: 1 }}>
{isRevealed ? <VisibilityOff /> : <Visibility />}
</IconButton>
)}
</Box>
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you should use the SecretField component here, instead of recreating it. That will simplify not only this code but also the logic of handling revealed secrets, which should rather be inside the component that displays them IMO.
I know our current SecretField doesn't handle copying but we could add that in a separate commit.

Copy link
Author

Choose a reason for hiding this comment

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

I have applied that here #2728 (comment)
Also, I have copying implemented there as well. If you want, I can move copy functionality inside of SecretField code.

Comment on lines 722 to 747
props?.env?.forEach(item => {
if (item.value) {
variables.set(item.name, {
value: item.value,
from: 'manifest',
isSecret: false,
isError: false,
});
} else if (item.valueFrom) {
const ref = item.valueFrom;
if (ref.secretKeyRef) {
processValueFromSecret(item.name, ref.secretKeyRef);
} else if (ref.configMapKeyRef) {
processValueFromConfigMap(item.name, ref.configMapKeyRef);
}
}
});

// Process env variables from envFrom (configMap or secret references)
props?.envFrom?.forEach(item => {
if (item.secretRef) {
processAllSecretKeys(item.secretRef.name, prefix);
} else if (item.configMapRef) {
processAllConfigMapKeys(item.configMapRef.name, prefix);
}
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

I am not sure this logic will not end up violating hooks' rules. Maybe @sniok can help review it.

Copy link
Contributor

Choose a reason for hiding this comment

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

You can only call hooks inside components or inside other hooks, these processX functions should be components: processValueFromSecret, processValueFromConfigMap, processAllSecretKeys, processAllConfigMapKeys, processValueFromFieldRef, processValueFromResourceFieldRef.

Copy link
Author

@vlasov-y vlasov-y Jan 7, 2025

Choose a reason for hiding this comment

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

I am not sure I have followed... I understand it like I have to move this functionality to separate functions that return React components and then use that components independently in ContainerEnvironmentVariables function. I think I can do that.

return data.from;
}
return (
<Link routeName={kind} params={{ name, namespace }}>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Usually we have done linking to resources by getting the details link from the respective class. See the makeOwnerReferences from https://github.com/headlamp-k8s/headlamp/blob/58c51a58932408d361ee725b1af27bcfc52df7ff/frontend/src/components/common/Resource/MetadataDisplay.tsx .

Copy link
Author

Choose a reason for hiding this comment

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

Resolved in 495f183
Review and resolve this conversation, please, if everything is alright.

frontend/src/components/common/Resource/Resource.tsx Outdated Show resolved Hide resolved
const processValueFromSecret = (name: string, secretRef: { name: string; key: string }) => {
const processValueFromSecret = (
name: string,
secretKeyRef: { name: string; key: string; optional?: boolean }
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't understand what optional is doing or where it is being assigned. Can you add more context to the commit message?

Copy link
Author

Choose a reason for hiding this comment

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

It is part of Kubernetes API. Some configMaps/secrets may be absent, but be optional in the pod manifest at the same time. In that case, if we receive 404 - that is not an issues.
https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#optional-configmaps

Comment on lines 792 to 809
case 'metadata.name':
value = pod.metadata.name || '';
break;
case 'metadata.namespace':
value = pod.metadata.namespace || '';
break;
case 'spec.nodeName':
value = pod.spec.nodeName || '';
break;
case 'spec.serviceAccountName':
value = pod.spec.serviceAccountName || '';
break;
case 'status.hostIP':
value = pod.status.hostIP || '';
break;
case 'status.podIP':
value = pod.status.podIP || '';
break;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Looks like this could be automated by checking whether we can split by . and checking 1st and 2nd split results from pod.
But probably a better solution is to use JSONPath to try to match the fieldPath.

Copy link
Author

Choose a reason for hiding this comment

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

Resolved in 3ef217d
Review and resolve this conversation, please, if everything is alright.

const resourceType = resourceFieldRef.resource;
const divisor = resourceFieldRef.divisor || '1';

const BINARY_SUFFIXES = new Map([
Copy link
Collaborator

Choose a reason for hiding this comment

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

We have similar logic in https://github.com/headlamp-k8s/headlamp/blob/f21da73a9def367461680227659befbe5ff2806f/frontend/src/lib/units.ts . Can you check if these are helpful to simplify the logic here?

Copy link
Author

Choose a reason for hiding this comment

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

Hmm.. That is close, but my logic supports more cases. I would suggest to keep mine. WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

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

It wouldn't be a good idea to keep two separate implementations of a pretty much the same thing. I'd suggest to either extend the existing logic to support more cases or reuse that logic as-is

@sniok sniok self-requested a review January 7, 2025 09:30
) => {
const [secret, setSecret] = React.useState<Secret | null>(null);
const [error, setError] = React.useState<ApiError | null>(null);
ResourceClasses.Secret.useApiGet(setSecret, secretKeyRef.name, namespace, setError);
Copy link
Contributor

Choose a reason for hiding this comment

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

we have a better way to do this now

const [secret, error] = Secret.useGet(secretKeyRef.name, namespace)

Copy link
Author

Choose a reason for hiding this comment

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

Resolved in 9da9ae2
Review and resolve this conversation, please, if everything is alright.

let value = '';
let isError = false;

try {
Copy link
Contributor

Choose a reason for hiding this comment

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

you can use JSONPath here, we're already using it in other places too, for example https://github.com/headlamp-k8s/headlamp/blob/main/frontend/src/components/crd/CustomResourceDetails.tsx#L79C7-L79C14

Copy link
Author

Choose a reason for hiding this comment

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

Resolved in 3ef217d
Review and resolve this conversation, please, if everything is alright.

Copy link
Contributor

@sniok sniok left a comment

Choose a reason for hiding this comment

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

This is a really great feature and thank you for taking time to implement it!

My biggest concern is that current implementation breaks the rules of hooks. I think this can be refactored into a bit different flow.

Instead of processing values and putting them into a variables map you could display values by using components like <ValueFromConfigMap configMapName={} configMapKey={} /> that can calls hooks and directly render those values. This will simplify things a bit and also avoid breaking the rules of hooks.

@vlasov-y
Copy link
Author

vlasov-y commented Jan 7, 2025

This is a really great feature and thank you for taking time to implement it!

My biggest concern is that current implementation breaks the rules of hooks. I think this can be refactored into a bit different flow.

Instead of processing values and putting them into a variables map you could display values by using components like <ValueFromConfigMap configMapName={} configMapKey={} /> that can calls hooks and directly render those values. This will simplify things a bit and also avoid breaking the rules of hooks.

That is awesome! I did not know you have such component.
Thank you for comments @sniok @joaquimrocha I will do my best to apply your recommendations shortly.
P.S. I am not a frontend developer, I am a DevOps, so frontend development is kind of unusual for me 😃
P.S.2. I will push new commits and once all our conversations are resolved I will squash them with rebase and push with force, so only one commit will be left

@vlasov-y
Copy link
Author

vlasov-y commented Jan 7, 2025

@sniok I will need you help.
#2728 (comment)
#2728 (review)
I have not followed the idea. I got that current architecture with calls of process functions is a bad approach, but I could not figure out how do you see it.
I try to stay with an idea that it must be one table with all environment variables. If move that process functions to separate components, they will produce separate InnerTables, won't they?
Also we have to consider that envrionment variables may override each other, because we may have variables with the same name in the container's .env and in the conrfigmap, for example.
Anyway, please, give me more information, or/and we can arrange a quick call so you would quickly example me what you want and I will be able to implement that.

@vlasov-y
Copy link
Author

vlasov-y commented Jan 7, 2025

@sniok @joaquimrocha Most of conversations have been updated. I have pushed many commits, but do not worry, I will squash them at the very end of the feature development. Please, resolve conversations that can be considered done.

@sniok
Copy link
Contributor

sniok commented Jan 7, 2025

@sniok I will need you help. #2728 (comment) #2728 (review) I have not followed the idea. I got that current architecture with calls of process functions is a bad approach, but I could not figure out how do you see it. I try to stay with an idea that it must be one table with all environment variables. If move that process functions to separate components, they will produce separate InnerTables, won't they? Also we have to consider that envrionment variables may override each other, because we may have variables with the same name in the container's .env and in the conrfigmap, for example. Anyway, please, give me more information, or/and we can arrange a quick call so you would quickly example me what you want and I will be able to implement that.

I thought a bit more about it and it might be simpler to stick to the current way. Let's name all the functions that use hooks inside to have prefix use, to make sure it's clear that they are hooks. processValueFromSecret, processValueFromConfigMap, processAllSecretKeys, processAllConfigMapKeys -> useValueFromSecret, useValueFromConfigMap, .... I know that if you're not familiar with react this change might seem to be pointless but it is a very important convention. And processEnvVariables function is immediately called after it's defined, so maybe let's just inline it. Now the issue is that there are hooks that are called in a loop, but since there's no way to add or remove env variables they'll stay the same (correct me if I'm wrong here though but I'm pretty sure you can't add/remove env variables on an existing container) and there won't be an issue with hooks here.

Some useful doc pages on this
https://react.dev/reference/rules/rules-of-hooks#only-call-hooks-at-the-top-level
https://react.dev/reference/rules/rules-of-hooks#only-call-hooks-from-react-functions

@dosubot dosubot bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Jan 7, 2025
@vlasov-y
Copy link
Author

vlasov-y commented Jan 7, 2025

FYI @sniok

Let's name all the functions that use hooks inside to have prefix use

Sure, resolved in d87ca5a

correct me if I'm wrong here though but I'm pretty sure you can't add/remove env variables on an existing container

Yes. In order to apply changed environment variables in manifest you have to recreate the pod. Does not matter whether you alter variables in configmap/secret or inline variables.

@vlasov-y vlasov-y requested review from sniok and joaquimrocha January 8, 2025 08:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
size:L This PR changes 100-499 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants