1717
1818import lombok .EqualsAndHashCode ;
1919import lombok .Value ;
20- import org .jspecify .annotations .Nullable ;
2120import org .openrewrite .ExecutionContext ;
2221import org .openrewrite .Option ;
2322import org .openrewrite .Recipe ;
2423import org .openrewrite .TreeVisitor ;
25- import org .openrewrite .docker .DockerIsoVisitor ;
2624import org .openrewrite .docker .trait .DockerFrom ;
27- import org .openrewrite .docker .trait .ImageName ;
28- import org .openrewrite .docker .tree .Docker ;
29- import org .openrewrite .internal .ListUtils ;
3025
31- import java .util .HashMap ;
3226import java .util .HashSet ;
33- import java .util .List ;
34- import java .util .Map ;
3527import java .util .Set ;
3628import java .util .regex .Matcher ;
3729import java .util .regex .Pattern ;
3830
3931import static java .util .Arrays .asList ;
40- import static java .util .Collections .emptyMap ;
41- import static java .util .Objects .requireNonNull ;
4232
4333@ EqualsAndHashCode (callSuper = false )
4434@ Value
@@ -60,208 +50,48 @@ public class UpgradeDockerImageVersion extends Recipe {
6050 private static final int OLDEST_VERSION = 8 ;
6151 private static final Pattern VERSIONED_TAG = Pattern .compile ("(\\ d{1,3})(\\ D.*)?" );
6252
63- private static final String ARG_DEFAULTS = "argDefaults" ;
64- private static final String ARG_UPGRADES = "argUpgrades" ;
65-
6653 String displayName = "Upgrade Docker image Java version" ;
6754 String description = "Upgrade Docker image tags to use the specified Java version. " +
6855 "Updates common Java Docker images including eclipse-temurin, amazoncorretto, azul/zulu-openjdk, " +
6956 "and others. Also migrates deprecated images (openjdk, adoptopenjdk) to eclipse-temurin, " +
70- "preserving any tag suffix such as `-jre-alpine`. When a `FROM` is built from a build argument, the " +
71- "default value of the corresponding global `ARG` is upgraded instead, such that `ARG java_version=17` " +
72- "used as `FROM eclipse-temurin:${java_version}` becomes `ARG java_version=25`. Image references built " +
73- "from arguments without a default value are left untouched, as their value can not be determined " +
74- "statically. A digest pin is dropped when the tag is upgraded, as the stale digest would otherwise " +
75- "keep resolving to the old image." ;
57+ "preserving any tag suffix such as `-jre-alpine`. Image references built from build arguments or " +
58+ "environment variables are left untouched, as their value can not be determined statically. A digest " +
59+ "pin is dropped when the tag is upgraded, as the stale digest would otherwise keep resolving to the " +
60+ "old image." ;
7661
7762 @ Override
7863 public TreeVisitor <?, ExecutionContext > getVisitor () {
7964 if (version == null ) {
8065 return TreeVisitor .noop ();
8166 }
82- return new DockerIsoVisitor <ExecutionContext >() {
83-
84- @ Override
85- public Docker .File visitFile (Docker .File file , ExecutionContext ctx ) {
86- Map <String , String > defaults = new HashMap <>();
87- for (Docker .Arg arg : file .getGlobalArgs ()) {
88- String value = arg .getValue () == null ? null : arg .getValue ().getText ();
89- if (value != null ) {
90- defaults .put (arg .getName ().getText (), value );
91- }
92- }
93-
94- Map <String , String > upgrades = new HashMap <>();
95- getCursor ().putMessage (ARG_DEFAULTS , defaults );
96- getCursor ().putMessage (ARG_UPGRADES , upgrades );
97- Docker .File f = super .visitFile (file , ctx );
98- if (upgrades .isEmpty ()) {
99- return f ;
100- }
101- return f .withGlobalArgs (ListUtils .map (f .getGlobalArgs (), arg -> {
102- String name = arg .getName ().getText ();
103- String upgraded = upgrades .get (name );
104- // A name may be declared more than once; only the declaration the default was read from moves
105- if (upgraded == null || arg .getValue () == null || !defaults .get (name ).equals (arg .getValue ().getText ())) {
106- return arg ;
107- }
108- return arg .withValue (withText (arg .getValue (), upgraded ));
109- }));
67+ return new DockerFrom .Matcher ().asVisitor ((image , ctx ) -> {
68+ String imageName = image .getImageName ().orElse ("" );
69+ String tag = image .getTag ().orElse ("" );
70+ if (containsVariable (imageName ) || containsVariable (tag )) {
71+ return image .getTree ();
11072 }
11173
112- @ Override
113- public Docker .From visitFrom (Docker .From from , ExecutionContext ctx ) {
114- if (containsVariable (from .getImageName ()) || containsVariable (from .getTag ())) {
115- return upgradeThroughArgs (from ,
116- getCursor ().getNearestMessage (ARG_DEFAULTS , emptyMap ()),
117- getCursor ().getNearestMessage (ARG_UPGRADES , new HashMap <>()));
118- }
119-
120- DockerFrom image = new DockerFrom (getCursor ());
121- String newTag = upgradedTag (image .getTag ().orElse ("" ));
122- if (newTag == null ) {
123- return from ;
124- }
125- String imageName = image .getImageName ().orElse ("" );
126- String newImageName = upgradedImageName (imageName );
127- if (newImageName == null ) {
128- return from ;
129- }
130- if (!newImageName .equals (imageName )) {
131- return image .withImageReference (newImageName + ":" + newTag );
132- }
133- return image .withTag (newTag ).withDigest (null );
74+ Matcher matcher = VERSIONED_TAG .matcher (tag );
75+ if (!matcher .matches ()) {
76+ return image .getTree ();
13477 }
135- };
136- }
137-
138- private Docker .From upgradeThroughArgs (Docker .From from , Map <String , String > defaults , Map <String , String > upgrades ) {
139- String imageVariable = soleVariable (from .getImageName ());
140- String tagVariable = from .getTag () == null ? null : leadingVariable (from .getTag ());
141- String imageName = imageVariable == null ?
142- from .getImageName ().getTextWithVariables () :
143- defaults .get (imageVariable );
144- if (imageName == null ) {
145- return from ;
146- }
147-
148- String tag ;
149- boolean wholeReference = from .getTag () == null ;
150- if (wholeReference ) {
151- // A single argument holding the whole reference, as in `FROM ${BASE_IMAGE}`
152- String [] reference = imageVariable == null ? null : splitReference (imageName );
153- if (reference == null ) {
154- return from ;
78+ int currentVersion = Integer .parseInt (matcher .group (1 ));
79+ if (currentVersion < OLDEST_VERSION || version <= currentVersion ) {
80+ return image .getTree ();
15581 }
156- imageName = reference [0 ];
157- tag = reference [1 ];
158- } else {
159- tag = tagVariable == null ? from .getTag ().getText () : defaults .get (tagVariable );
160- }
161- if (tag == null ) {
162- return from ;
163- }
164-
165- String newImageName = upgradedImageName (imageName );
166- String newTag = upgradedTag (tag );
167- if (newImageName == null || newTag == null ) {
168- return from ;
169- }
17082
171- if (wholeReference ) {
172- upgrades .put (requireNonNull (imageVariable ), newImageName + ":" + newTag );
173- return from .withDigest (null );
174- }
175- if (tagVariable == null ) {
176- from = from .withTag (withText (requireNonNull (from .getTag ()), newTag ));
177- } else {
178- upgrades .put (tagVariable , newTag );
179- }
180- if (!newImageName .equals (imageName )) {
181- if (imageVariable == null ) {
182- from = from .withImageName (withRepository (from .getImageName (), imageName , newImageName ));
183- } else {
184- upgrades .put (imageVariable , newImageName );
83+ String newTag = version + (matcher .group (2 ) == null ? "" : matcher .group (2 ));
84+ if (DEPRECATED_IMAGES .contains (imageName )) {
85+ return image .withImageReference (NEW_IMAGE + ":" + newTag );
18586 }
186- }
187- return from .withDigest (null );
188- }
189-
190- private @ Nullable String upgradedImageName (String imageName ) {
191- ImageName parsed = ImageName .parse (imageName );
192- String path = parsed .getPath ();
193- if (DEPRECATED_IMAGES .contains (path )) {
194- String registry = parsed .getRegistry ();
195- return registry == null ? NEW_IMAGE : registry + '/' + NEW_IMAGE ;
196- }
197- return CURRENT_IMAGES .contains (path ) ? imageName : null ;
198- }
199-
200- private @ Nullable String upgradedTag (String tag ) {
201- Matcher matcher = VERSIONED_TAG .matcher (tag );
202- if (!matcher .matches ()) {
203- return null ;
204- }
205- int currentVersion = Integer .parseInt (matcher .group (1 ));
206- if (currentVersion < OLDEST_VERSION || version <= currentVersion ) {
207- return null ;
208- }
209- return version + (matcher .group (2 ) == null ? "" : matcher .group (2 ));
210- }
211-
212- private static boolean containsVariable (Docker .@ Nullable Argument argument ) {
213- return argument != null && argument .hasEnvironmentVariables ();
214- }
215-
216- private static @ Nullable String soleVariable (Docker .Argument argument ) {
217- List <Docker .ArgumentContent > contents = argument .getContents ();
218- return contents .size () == 1 && contents .get (0 ) instanceof Docker .EnvironmentVariable ?
219- ((Docker .EnvironmentVariable ) contents .get (0 )).getName () : null ;
220- }
221-
222- private static @ Nullable String leadingVariable (Docker .Argument argument ) {
223- List <Docker .ArgumentContent > contents = argument .getContents ();
224- if (contents .isEmpty () || !(contents .get (0 ) instanceof Docker .EnvironmentVariable )) {
225- return null ;
226- }
227- for (int i = 1 ; i < contents .size (); i ++) {
228- if (!(contents .get (i ) instanceof Docker .Literal )) {
229- return null ;
230- }
231- }
232- return ((Docker .EnvironmentVariable ) contents .get (0 )).getName ();
233- }
234-
235- private static String @ Nullable [] splitReference (String reference ) {
236- int at = reference .indexOf ('@' );
237- String withoutDigest = at == -1 ? reference : reference .substring (0 , at );
238- int colon = withoutDigest .indexOf (':' , withoutDigest .lastIndexOf ('/' ) + 1 );
239- if (colon == -1 ) {
240- return null ;
241- }
242- return new String []{withoutDigest .substring (0 , colon ), withoutDigest .substring (colon + 1 )};
243- }
244-
245- /// The registry an image is pulled from is left as written, which may be a variable, so only the trailing
246- /// repository is rewritten.
247- private static Docker .Argument withRepository (Docker .Argument imageName , String from , String to ) {
248- String oldPath = ImageName .parse (from ).getPath ();
249- String newPath = ImageName .parse (to ).getPath ();
250- return imageName .withContents (ListUtils .mapLast (imageName .getContents (), content -> {
251- if (!(content instanceof Docker .Literal )) {
252- return content ;
87+ if (CURRENT_IMAGES .contains (imageName )) {
88+ return image .withTag (newTag ).withDigest (null );
25389 }
254- Docker .Literal literal = (Docker .Literal ) content ;
255- String text = literal .getText ();
256- return text .endsWith (oldPath ) ?
257- literal .withText (text .substring (0 , text .length () - oldPath .length ()) + newPath ) :
258- literal ;
259- }));
90+ return image .getTree ();
91+ });
26092 }
26193
262- private static Docker .Argument withText (Docker .Argument argument , String text ) {
263- return argument .withContents (ListUtils .mapLast (argument .getContents (),
264- content -> content instanceof Docker .Literal ? ((Docker .Literal ) content ).withText (text ) : content ));
94+ private static boolean containsVariable (String imageReferencePart ) {
95+ return imageReferencePart .indexOf ('$' ) != -1 ;
26596 }
266-
26797}
0 commit comments