Repository navigation
Proposal to swap deprecated request for node-fetch #754
Description
Activity
This approach is going to introduce breaking changes for consumers of this client library (there were 151k downloads last week alone).
We need to have a more careful plan for how we roll out this change.
I would like to support existing users for at least a few versions (perhaps 3?) while we make this transition.
Perhaps the right way to do this is to introduce a new major version to signify the new generator?
e.g.
0.17.x== old generator, Kubernetes 1.23 API
0.18.x== old generator, Kubernetes 1.24 API
0.19.x== old generator, Kubernetes 1.25 API
Support for old generator stops after 1.251.0.x== new generator, Kubernetes 1.23 API
1.1.x== new generator, Kubernetes 1.24 API
1.2.x== new generator, Kubernetes 1.25 API
Support for subsequent kubernetes versions continues with the new generator.This will require introducing a new branch for the
1.xseries of releases, that's probably going to be necessary anyway since we don't want to make this change in a single PR.Please note that this is going to be a big change that will touch nearly the entire library, I think
That phasing plan makes sense to me. It's in-line with the npm semantic versioning.
It might make sense to deprecate the 0.x releases with a warning telling them that support for the old client api will no longer be supported after a certain version and encouraging them to upgrade. That should alert existing users of the changes.
I'm prepared to take on the responsibility associated with this upgrade.
Reacted by David GameroIs using both generators and fetch/request in parallel not an option during the transition? I haven't thought through all implications or the workload of doing that, just thinking out loud, but it would be nice if you could do something like
makeApiClient(CoreV1Api, { fetch: true })for the next 3 releases or smth, and only do the breaking change then?I agree it would be nice if we could keep the user experience that cohesive, but unfortunately these changes seem too broad and breaking for that to be doable. Given the extent of the proposed changes, it could introduce significant overhead and complexity to have two major versions bundled in a single release.
I've taken a shot at incrementing
OPENAPI_GENERATOR_COMMITas proposed, and it appears that several response types have changed, an example is the removals of the.bodyproperties ex:(await api.listNamespacedPod(namespace)).body->(await api.listNamespacedPod({ namespace }))Another predictable area of major changes is the error interfaces since we are changing the network library, and the proposal includes incrementing typescript by a major version.
This may be a config option that I've inadvertently changed, but IMO it does come across cleaner.
This could be a concern if we had
{ fetch: true }as an option, since the option wouldn't just change functionality under the hood, but also several parts of the api interfaces.I'll keep checking it out. There could be an easier way tweaking the generator options that I'm not familiar with.
Reacted by Oliver KingOne other option would be to package the old generated code and the new generated code into different directories and effectively have:
import api_old from @kubernetes/node-clientor
import api_new from @kubernetes/node-cliente.g. keep everything in one place from a release perspective, but introduce a fork in the imports that you can use.
That could definitely work if we introduce a named export for
api_old. I'm not sure how we could do that without potentially breaking existing default import compatibility unless we nest theapi_newinside it.Another possibility would be to have
import api_old from @kubernetes/node-clientor
import api_new from @kubernetes/node-client/fetchto retain existing import compatibility for
import k8s from @kubernetes/node-client- Current Top Level Exports
// ✅ `import { CoreV1Api } from @kubernetes/node-client` // ✅ `import k8s from @kubernetes/node-client` @kubernetes/node-client ├── CoreV1Api ... └── KubeConfig- Named Level Exports
// ✅ `import { api_new } from @kubernetes/node-client` // ✅ `import { api_old } from @kubernetes/node-client` // ❌ `import k8s from @kubernetes/node-client` <- no longer works @kubernetes/node-client ├── api_old │ ├─ CoreV1Api │ ... │ └─ KubeConfig │ └── api_new ├─ CoreV1Api ... └─ KubeConfig- Messy Nested Export
// ✅ `import { api_new } from @kubernetes/node-client` // ✅ `import { api_old } from @kubernetes/node-client` // ✅ `import k8s from @kubernetes/node-client` <- works still, but gross way to do it @kubernetes/node-client ├── CoreV1Api ... ├── KubeConfig └── api_new ├─ CoreV1Api ... └─ KubeConfigI think there are different kinds of breaking changes. I feel like changing an import statement is a much lower cost breaking change vs changing the entire API surface area. So I would be ok to require
import { api_new as api } from @kubernetes/node-clientor
import { api_old as api } from @kubernetes/node-clientAnd I think that is feasible...
Reacted by David Gamero, Ivan and Paul ArahI don't think putting both the new api and old api in the same package / directory makes sense to me.
The fact that it's a breaking change between them means that someone can't just swap out API's when making the change.
// switching from this import { api_new as api } from @kubernetes/node-client // to this would still require code changes in lots of places where the api is used import { api_old as api } from @kubernetes/node-client
Someone migrating to the new api would have to change every single node-client import statement in their repo which would be time consuming and tedious. It's simpler from a user-standpoint to simply upgrade their version then fix any errors in the code.
By having both the old and new api in the same package we also wouldn't be able to take advantage of a npm deprecation warning which would help warn users of the upcoming drop of support for the old api.
Users wouldn't be upgraded to the breaking api without doing it manually (or changing their
package.jsonto allow for major release updates) due to npm semantic versioning.From a user-standpoint I don't think it's easier to change the import and I believe one of our goals should be gently pushing consumers to migrate when they get the chance (or at least be aware of upcoming drops in support). From our standpoint it's more difficult to work with a directory layout that has two apis rather than just different branches. Storing the new and old api in the in the same package would make more sense to me if there was some reason why a user would want to use both the old and new api but I can't think of one.
Reacted by David Gamero, Ștefan Cenușă and Sebastian MaltonThe major version suggestion seems be the cleanest way.
Putting the changes in a new major version would make upgrading a clear decision without interfering with existing code.
That way we don't mix code from two generators in a single package.
Reacted by Oliver King, Ștefan Cenușă and IvanCompletely supporting the major version upgrade. Our team doesn't mind spending some time adjusting code to the new API, as long as we can stop using deprecated or regularly vulnerable packages! 😄
Reacted by David Gamero and Oliver KingOk, sounds like the consensus is for a major version upgrade. I have created a
release-0.xbranch, which we will use for the releases of the old generated client for the next ~9 months.We can start sending PRs to the
masterbranch now to start making these changes.Reacted by Dominykas Blyžė and Juan Carlos RomanWe should also probably turn this discussion into a markdown document that describes our approach and links off of the main README.md
Reacted by David Gamero and Oliver KingQuick update: @davidgamero and I have been working on this for the last two weeks. We're having to make some changes to open-api generator to support the way we need to do auth. @davidgamero has been working on identifying the exact improvements necessary.
Reacted by David Gamero, Juan Carlos Roman and Marcio SaegerUpdate 2: Two PRs have been completed against OpenAPITools/openapi-generator (the typescript generator template in specific), and a third is in progress.
Complete (PR)
- [typescript] Default auth method support and optional param object when all params optional
- useSingleRequestParameter should mark parameter optional if all properties are optional
In Progress (Issue) [REQ][typescript] Enable passing custom SecurityAuthentication to API
With this last PR, the ability to pass certificates to the
fetchclient should be exposed in the typescript generator, clearing the way for full auth migrationReacted by Oliver KingReacted by IvanUpdate 3: Custom Authentication is now complete in the typescript generator. Currently migrating the
requestimplementations outside the generated code (in /src)54 remaining items
Migrated 14 microservices to rc4 with no issues other than one TS type that needed to change (we had cast the watch result
anyin 0.20 to a request type and it needed to beAbortControllernow -- which makes more sense anyway)Reacted by Max Strübing, Yams, Daniel Lamando and William SedlacekThe latest pre-release library https://www.npmjs.com/package/@kubernetes/client-node/v/1.0.0-rc4?activeTab=versions remains rc4 made available on 2023-12-23, which appears to be receiving positive feedback for migrating microservices.
Is there any further thought when v1.0.0 maybe released and in which kubernetes version it will be included? I believe this is blocking the remediation of CVE-2023-28155 as well as CVE-2023-26136, where it seems the required upgrade to tough-cookie-4.1.3 is dependent on the replacement of the request package.
PSIRTRef_fup_3P_kubernetes PVR0429050 PVR0452658 PVR0473928 PVR0473931
I wouldn't say that rc4 feedback is positive. Yes, code works properly but it is a pain to use middleware. For example when you need to add a header when patching pod:
import * as k8s from '@kubernetes/client-node'; ... const kc = new k8s.KubeConfig(); kc.loadFromDefault(); const headerPatchMiddleware = new PromiseMiddlewareWrapper({ pre: async (requestContext) => { requestContext.setHeaderParam('Content-type', 'application/json-patch+json'); return requestContext; }, post: async (responseContext) => responseContext, }); const currentCluster = kc.getCurrentCluster(); if (!currentCluster) { throw new Error('Kube config does not have current cluster.'); } const server = currentCluster.server; if (!server) { throw new Error('Kube config cluster does not have server.'); } const baseServerConfig: k8s.ServerConfiguration<Record<string, never>> = new k8s.ServerConfiguration<Record<string, never>>(server, {}); const k8sPatchPodConfiguration = k8s.createConfiguration({ middleware: [headerPatchMiddleware], baseServer: baseServerConfig, authMethods: { default: kc }, }); const k8sApi = kc.makeApiClient(k8s.CoreV1Api); ... const patch = [ { op: 'replace', path: '/metadata/annotations', value: annotationsObject, }, ]; await k8sApi.patchNamespacedPod( { name: podName, namespace: namespace, body: patch, }, k8sPatchPodConfiguration, );I think you agree that we need quite much code to patch a pod, it should be simpler...
Reacted by Max Strübing, Marcio Saeger, Daniel Lamando and kooogeI'm not sure where the best place to return feedback on RC4 is, but I'll assume from the thread maybe here.
The errors being returned appear to have a body of a string or undefined, and not an object as they were in 0.2.0:
const config = new KubeConfig() config.loadFromDefault() try { config.makeApiClient(CoreV1Api).createNamespace({ body: { metadata: { name: 'k8s will 💩 itself' } } }) } catch (error) { if (!(error && typeof error === 'object' && error instanceof ApiException && 'body' in error)) { throw error } expect(error.body).toHaveProperty('reason') // fails in 1.0.0 but not 0.2.0 //instead need to parse the body if (typeof error.body === 'string') { // it's also possible to get `undefined` error body const errorBody: unknown = JSON.parse(error.body) expect(error.body).toHaveProperty('reason') //passes } }Also not sure where the 1.x feedback should go, but on top of the few already reported issues above here are few pain points from my migration to 1.x:
- Missing
application/apply-patch+yamlfromPatchStrategyenum. Working around this requires use of middleware PromiseMiddlewareWrappernot being exported, requiring user to deal with Promise -> Observable conversion (I might be completely wrong here tho)ObjectSerializerno longer being exported, which my custom cache/watch implementation was relying on
- Missing
Just pointing out that fetch is implemented natively in Node.js, and it's been available since Node 18 (so it includes current and previous LTS, so strictly there's no need to use
node-fetch. Of course, if the project wants to support earlier versions of Node.js, thennode-fetchmakes sense.Reacted by Daniel Lamando and Vito MacchiaNode 16 went EOL in September of 2023, so it's doesn't really feel valuable supporting it—especially since the standard k8s support window is only 14 months. Is there interest from the team in going straight to the node implementation of fetch or is the plan to stick with node-fetch?
Edit: It's worth noting that while native fetch was pulled out from behind the
--experimental-fetchflag in v18, it wasn't marked as stable until v21 (v22 LTS). See Node API globals.I would think we should start with finishing the
node-fetchimplementation.
Later on we can change that to the native fetch implementation. That shift should be significant easier than the move fromrequesttonode-fetchReacted by Lachlan Heywood and David RüschhoffReacted by Vito Macchia@mstruebing do you think this issue can be closed since v1.0.0 was released with node-fetch?
Reacted by Daniel Sutton@dastrobu we have had an issue with our GitHub action so the release did not get tagged on GitHub.
It’s available via npm.
We could manually push that tag to GitHub and create a GitHub release.
I will do that later if I can find out the exact commit hash.Reacted by Daniel StrobuschThanks for the patience, added the tag at the appropriate commit:
https://github.com/kubernetes-client/javascript/releases/tag/1.0.0
As determined by the logs from the GH Action that pushed to NPM:
Reacted by Mateusz Wroński and RSAldermanClosing, as the swap happened.
/close
Reacted by Joshua Perry@cjihrig: Closing this issue.
Details
In response to this:
Closing, as the swap happened.
/close
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.
Proposal to swap deprecated request for node-fetch
Request is fully deprecated requiring us to switch libraries (see #414 for more information).
@brendanburns outlined some of the clear options forward. This change should ideally be a long-term solution leaving us with the option of either modifying the node-typescript open-api-generator to use a different request library or migrate to one of the other generator options.
Modifying an outdated node-typescript library does not seem like the right path forward. It would require us to still use a new library which would likely change the api and still be a breaking change. We might as well migrate to a newer version of openapi-generator while we do that and acquire the advantages of a more up-to-date version.
Choosing a new library
@d-cs posted a nice summary of the different choices. We narrowed it down to Axios or Fetch. The other libraries didn't match how widely adopted and focused these libraries are.
Fetch is simply the standard JavaScript as a whole is moving to, which brings more longterm support aligning us better with our goal of migrating to a long-term solution. node-fetch has significantly more weekly downloads than axios. The package size of node-fetch is also noticeably smaller than axios.
Additionally, future improvements could likely be done to allow us to also support the browser #165 since Fetch is native to the browser.
Node-fetch
Fetch is a native browser API that is not implemented in Node.js by default. Node-fetch is the natural choice for the Node implementation because it is the most widely adopted.
The openapi-generator uses the browser implementation of Fetch so we must substitute node-fetch in manually.
Implementation steps
@davidgamero and I will work to implement the following changes.
Other repositories
typescriptThreePlusconfig option 1 2Kubernetes-client repository
OPENAPI_GENERATOR_COMMITto be version 5.3.0npm install node-fetchto install node-fetchsrc/api.tswith the followingnpm run generate